HTTP Redirects
A 3xx response that sends the client to a different URL, named in the Location header, instead of returning the content it asked for. Each redirect costs a full round trip, and a chain pays that cost once per hop before anything renders.
Also known as URL redirection, URL forwarding.
Full Explanation
An HTTP redirect is a 3xx response. It does not carry the representation the client asked for. Instead it names a different URL in the Location header (RFC 9110, section 15.4). The client must then issue a whole new request to get the content. That second request is another round trip. When the redirect changes the host, it also costs a fresh DNS lookup, a TCP connection and a TLS handshake. The response body is not empty. It is usually a short hypertext note linking to the new URL. But it is never the thing that was requested.
Three things a redirect is not. First, it is not mandatory for the client. A user agent MAY follow the Location it is given. Browsers do this by default, but nothing in the standard requires it. Second, it is not the whole 3xx class. 304 (Not Modified) points a conditional request at the client's own cached copy. 300 (Multiple Choices) offers a list for the user to choose from. Neither is a Location-following redirect (RFC 9110, section 15.4). Third, it is not a routing mechanism. Steering visitors to a nearer region with a redirect charges every one of them a round trip that geo-DNS or anycast would not. For CDN work, the entry compresses to one line. A redirect is a round trip. You can move it to the edge or delete it outright. Two questions pick the status code: is the move permanent, and does the request method have to survive it?
How it works
The 3xx (Redirection) class "indicates that further action needs to be taken by the user agent in order to fulfill the request". A server sends one when the requested URL is not the URL that should return content. The resource moved, the scheme or host changed, or the answer lives somewhere else. If a Location header is present, the user agent MAY redirect automatically "even if the specific status code is not understood". The specification warns that automatic redirection "needs to be done with care for methods not known to be safe" (RFC 9110, section 15.4).
A user agent that does follow a redirect SHOULD resend the original request with a defined set of modifications. It replaces the target URI with the Location value resolved against the original target URI. It changes the method if the status code's semantics call for it. It also removes header fields that no longer apply: Host, Referer, Origin, Authorization and Cookie among them, plus any validators the cache added (RFC 9110, section 15.4). That last rule is why a redirect across hosts quietly loses credentials.
The codes divide on two axes: permanent or temporary, and method-preserving or not.
- 301 (Moved Permanently): the target "has been assigned a new permanent URI and any future references to this resource ought to use one of the enclosed URIs". For historical reasons, a user agent MAY change the method from POST to GET. So 301 is unambiguous for GET traffic and ambiguous for anything else (RFC 9110, section 15.4.2).
- 308 (Permanent Redirect): the same permanent meaning, added alongside 307 "to unambiguously indicate method-preserving redirects". Use it when a permanent move must keep the method and body. It dates from June 2014. The specification notes it "might not be recognized everywhere" (RFC 9110, section 15.4.9).
- 302 (Found): the resource "resides temporarily under a different URI". Since the redirect may be altered later, "the client ought to continue to use the target URI for future requests". It too MAY rewrite POST to GET (RFC 9110, section 15.4.3).
- 307 (Temporary Redirect): temporary, and the user agent "MUST NOT change the request method if it performs an automatic redirection to that URI". This is the temporary code to reach for when a POST must survive (RFC 9110, section 15.4.8).
- 303 (See Other): not a move at all. It points at a different resource that gives an indirect answer, fetched with GET or HEAD. The new URI "is not considered equivalent to the target URI". Its primary use is redirecting the output of a POST to a separately addressable page. That way a browser refresh redisplays that page instead of replaying the operation (RFC 9110, section 15.4.4, MDN).
Location resolution is specified rather than conventional. The field value is a single URI-reference. When it is a relative reference, "the final value is computed by resolving it against the target URI" (RFC 9110, section 10.2.2). A relative Location therefore keeps the current scheme and host. That is what makes a path-only redirect table portable across every domain on one service.
Caching a redirect follows the ordinary storage rules. 301 and 308 are on the list of heuristically cacheable status codes. A cache may store and reuse them with heuristic expiration "unless otherwise indicated by the method definition or explicit cache controls". 302, 303 and 307 are absent from that list, and "all other status codes are not heuristically cacheable" (RFC 9110, section 15.1). A cache MUST NOT store a response that carries none of the permitted storage signals. So a 302 or 307 is only kept when the response supplies one explicitly: a Cache-Control public or max-age directive, s-maxage for a shared cache, or an Expires header (RFC 9111, section 3). Two redirects that look identical to a visitor can therefore behave completely differently in caches and browsers.
Loops are the client's job to catch. "A client SHOULD detect and intervene in cyclical redirections". Chain length is bounded in practice too. An earlier revision of HTTP recommended a maximum of five redirections, and "some clients might implement such a fixed limitation" (RFC 9110, section 15.4).
Why it matters for a CDN
A redirect is pure latency. Nothing the visitor asked for arrives on the first request. One hop is "the small performance hit of an additional round-trip". For a link you control, MDN is blunter: "a redirect has a significant performance cost (as an extra HTTP request occurs)". MDN advises fixing the link instead (MDN). A chain pays the cost per link. HTTP to HTTPS to www to the final URL is three waits before the first byte of content. Every hop that changes the host adds connection setup and a TLS handshake on top of the round trip.
Where the redirect is answered decides who pays for it. Answered at the edge, the hop is a short round trip to a nearby POP. The origin never sees the request. Akamai's framing is that handling it at the edge saves "a round-trip to the origin and any resulting hits against your infrastructure" (Akamai). Answered at the origin, every redirect is a full origin round trip that returns no content. There is a billing edge as well. On CloudFront, "when a viewer makes an HTTP request that is redirected to an HTTPS request, CloudFront charges for both requests" (AWS).
What CDNs do
- Cloudflare: Always Use HTTPS "redirects all your visitor requests from http to https, for all subdomains and hosts in your application", and is available on every plan. Cloudflare "recommends not performing redirects at your origin web server, as this can cause redirect loop errors" (Cloudflare). Single Redirects build static or dynamic URL redirects from wildcard patterns. String replacement and regular expressions are available depending on plan. Bulk Redirects "allow you to define a large number of URL redirects at the account level, which can apply across domains in your account". But they are static: no string replacement, no regular expressions.
- Fastly: with Force TLS, "any unencrypted request to your website returns a 301 Moved Permanently response which redirects to the TLS equivalent" (Fastly). Arbitrary redirects are written in VCL. Fastly's own recipe keeps the mapping in a VCL table or edge dictionary, raises a synthetic 618 error on a match, then converts "the synthetic object from an 618 to a 308 response code" with a constructed
Locationheader (Fastly). - CloudFront: the Redirect HTTP to HTTPS viewer protocol policy returns "HTTP status code 301 (Moved Permanently) along with the new HTTPS URL" for GET and HEAD. It returns 307 (Temporary Redirect) for POST, PUT, DELETE, OPTIONS and PATCH so that the method and body survive. That 307 applies only when the request is HTTP/1.1 or above. The same methods below HTTP/1.1 get 403 (Forbidden) instead (AWS). Rule-driven redirects are written as edge functions. A CloudFront Function on the viewer request event can return a 3xx response itself.
- Akamai: the Edge Redirector Cloudlet "helps you more efficiently manage large numbers of redirects" from the edge. It supports thousands of redirects for a single property. It exposes a basic editor role limited to the incoming URL, the redirect URL and a 301 or 302 status (Akamai).
Watch out for
- Permanent means permanent. A 301 or 308 is heuristically cacheable. So browsers and shared caches can hold a wrong mapping long after you fix it. The specification even invites a user agent "with link-editing capability" to rewrite references permanently (RFC 9110, section 15.4.2). Ship a new permanent redirect as a 302 or 307 first, then promote it.
- Redirect loops. The classic cause is not a bad rule but a mismatch between two layers. With Cloudflare's Flexible encryption mode, the edge reaches the origin over plain HTTP, and "redirect loops will occur if your origin server automatically redirects all HTTP requests to HTTPS". Conflicting redirect rules at the edge do it too. The browser gives up with ERR_TOO_MANY_REDIRECTS (Cloudflare).
- Credentials vanish across hosts. Authorization and Cookie are among the fields a user agent strips or reconsiders when it follows a redirect. So redirecting an authenticated API call to a different host turns it into an anonymous one.
- Chains nobody designed. HTTP-to-HTTPS, www normalisation and trailing-slash or lower-casing rules are each one hop. Written independently, they compose into three. Collapse them into one rule that emits the final URL.
- Redirects are not routing. Sending users to a regional hostname charges every visitor a round trip. A cached 301 pins them to that region. Route with geo-DNS or anycast, which cost nothing extra at request time.
- HSTS changes the picture but does not replace the redirect. Once a browser holds an HSTS policy for the host, it "MUST replace the URI scheme" with https before loading any http URL. This applies even when following a redirect, so the HTTP-to-HTTPS hop never reaches the wire (RFC 6797, section 8.3). Keep the redirect anyway. As Fastly puts it, "requests can still happen over HTTP first". The first visit, before the policy is known, still needs it.
Best practice
- One hop, maximum: emit the final URL directly instead of chaining scheme, host and path fixes.
- Answer the HTTP-to-HTTPS and canonical-host hops at the edge, never at the origin.
- Use 301 for a permanent move of GET traffic, and 308 when the method and body must survive it.
- Use 302 or 307 for anything temporary, and 307 specifically when a POST must be preserved.
- Use 303 after a POST, PUT or DELETE, so that a refresh cannot replay the operation.
- Put explicit Cache-Control on any redirect whose lifetime matters, including a short max-age on a 301 you are not yet certain of.
- Send
Locationas a relative reference when the host is unchanged, so one rule serves every domain on the service. - Verify each flow end to end with curl -sIL, reading the status and
Locationof every hop rather than only the final 200.
Examples
Remove redirect chains in Nginx:
# BAD: two separate redirects (chain)
server {
listen 80;
return 301 https://$host$request_uri; # redirect 1
}
server {
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri; # redirect 2
}
# GOOD: single redirect to final destination
server {
listen 80;
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 80;
server_name example.com;
return 301 https://example.com$request_uri;
}
Check for redirect chains:
curl -sIL https://www.example.com/ 2>&1 | grep -E "(HTTP/|Location:)"
# HTTP/2 301 Location: https://example.com/
# HTTP/2 200
Frequently Asked Questions
A 3xx response that sends the client to a different URL, named in the Location header, instead of returning the content it asked for. Each redirect costs a full round trip, and a chain pays that cost once per hop before anything renders.
Remove redirect chains in Nginx:
# BAD: two separate redirects (chain)
server {
listen 80;
return 301 https://$host$request_uri; # redirect 1
}
server {
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri; # redirect 2
}
# GOOD: single redirect to final destination
server {
listen 80;
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 80;
server_name example.com;
return 301 https://example.com$request_uri;
}
Check for redirect chains:
curl -sIL https://www.example.com/ 2>&1 | grep -E "(HTTP/|Location:)"
# HTTP/2 301 Location: https://example.com/
# HTTP/2 200
Yes. HTTP Redirects is also known as URL redirection, URL forwarding. A 3xx response that sends the client to a different URL, named in the Location header, instead of returning the content it asked for. Each redirect costs a full round trip, and a chain pays that cost once per hop before anything renders.