Fixing redirect chains
Updated ·16 min read
A redirect is a server telling a client that what it asked for lives somewhere else: a 3xx status code and a Location header naming the new address. One of them is ordinary and useful. Three or four of them in a row, which is what most sites end up with by accident, is a chain — and a chain is a series of full round trips that every visitor pays for before a single byte of the page arrives.
Chains are not usually designed. They accumulate. Someone adds an HTTPS redirect, someone else adds a www redirect, a CMS adds a trailing-slash rule, and nobody ever traces the result end to end because a browser hides the intermediate steps. The address bar shows the final URL and everything looks fine.
This guide covers what the different 3xx codes actually mean, why the difference between 301 and 308 matters for anything that is not a plain page view, what each extra hop costs, the canonical ordering that collapses the usual four hops into one, and how to diagnose and fix the result on nginx, Apache and Cloudflare.
Check yours now
What a chain is, and what a loop is
A chain is more than one redirect between the URL a visitor requested and the page they finally get. Requesting http://example.com/pricing and being sent to https://example.com/pricing, then to https://www.example.com/pricing, then to https://www.example.com/pricing/ is three hops: three requests, three responses, and only then the page.
A loop is a chain that comes back to a URL it has already visited. The browser follows hops until it hits its own limit and then stops with an error — Chrome and Firefox both give up after twenty. Nothing at that address loads for anyone, and search engines drop the URL rather than keep retrying.
Loops almost always come from two rules that were each correct in isolation. The classic one is a proxy that terminates TLS and forwards to the origin over plain HTTP, while the origin has its own rule redirecting plain HTTP to HTTPS: the proxy keeps handing the origin a plaintext request, and the origin keeps sending it back. The fix is to make the origin read X-Forwarded-Proto rather than testing the scheme of the connection it received. The other common pair is a www rule and a non-www rule that each point at the other, usually because one lives in the web server and the other in the application.
The five redirect codes, and why the method is the point
RFC 9110 defines redirection in section 15.4. Five codes are in ordinary use, and the difference that matters between them is not really permanence — it is whether the client is allowed to change the request method when it follows the redirect.
- 301 Moved Permanently (RFC 9110 section 15.4.2). The target has a new permanent URL. Clients and search engines may cache it and stop asking for the old one. Critically, the specification says that for historical reasons a user agent may change the request method from POST to GET when it follows a 301 — and in practice every browser does exactly that.
- 302 Found (section 15.4.3). Temporary. The client should keep using the original URL for future requests. It carries the same historical method-rewriting allowance as 301.
- 303 See Other (section 15.4.4). Tells the client to fetch a different resource with GET, whatever method the original request used. This is the correct answer to a form submission: it turns a POST into a GET of a result page, so a refresh does not resubmit the form.
- 307 Temporary Redirect (section 15.4.8). Temporary, and the method and body must not be changed. It is 302 with the ambiguity removed.
- 308 Permanent Redirect (section 15.4.9). Permanent, and the method and body must not be changed. It is 301 with the ambiguity removed.
So the pairs are 301 and 308 for permanent, 302 and 307 for temporary, with the second of each pair preserving the request method. For an ordinary page link the distinction never shows, because the request was a GET and stays a GET. It shows the moment something POSTs: an API client, a webhook receiver, a form target. A POST that meets a 301 can arrive at the destination as a GET with the body discarded, and the caller sees a confusing empty result rather than an error.
Use 301 for the canonicalisation redirects a site has permanently: plain HTTP to HTTPS, and the non-canonical host to the canonical one. These are permanent by definition — you are not going to change your mind about whether your site is at www — and permanence is what lets browsers and search engines stop re-requesting the old form.
Use 308 wherever the request method matters, which in practice means API endpoints and anything that receives POSTs. If a hostname serves both a website and an API, 308 is the safer choice across the board: it behaves identically to 301 for GET traffic and does not silently break POSTs.
Use 302 or 307 only for something genuinely temporary — a maintenance page, an A/B test, a short-lived campaign URL. Our scanner reports a 302 on a www or HTTPS canonicalisation redirect as a finding for exactly this reason: a temporary status is not cached, so every visitor pays for the hop on every visit, and search engines keep both forms in the index rather than consolidating onto one.
Use 303 after a form submission, to move the browser from the POST to a GET of the result page.
One caution about 301 that is worth stating plainly before you deploy one: browsers cache permanent redirects aggressively, and a 301 you later regret can persist in a visitor's browser long after you have removed it, with no way for you to clear it. Roll a new 301 out behind a short cache lifetime first, confirm the destination is right, and only then let it settle.
What each extra hop actually costs
A redirect is not a rewrite inside the server. It is a full response to the client followed by a completely new request, and the client has to do everything again: possibly a DNS lookup if the hostname changed, a TCP connection, a TLS handshake, and then the round trip for the request and response themselves.
On a fast desktop connection that is tens of milliseconds and nobody notices. On a mobile network with 150ms of round-trip latency, a hop that also changes hostname can cost half a second before anything starts loading, and the cost lands entirely on the part of the page load the visitor experiences as waiting for a blank screen. Two unnecessary hops on a four-hop chain is a second of nothing happening.
It matters more than it used to because it lands directly on Largest Contentful Paint: nothing on the page can begin loading until the final response starts arriving, so every redirect hop shifts the whole waterfall to the right. Our redirect checker records the timing of each hop separately, which is the fastest way to see whether one particular step — usually the one that crosses hostnames — is doing most of the damage.
There is a correctness cost too. Google's documentation states that its crawlers follow up to 10 redirect hops by default, and its site-move guidance says to redirect to the final destination directly, keeping any chain to ideally no more than three hops and fewer than five. Every hop is also another place a query string or a path segment can be dropped by a rule that was written for the home page.
The canonical ordering, and the four-hop mistake
Decide the final form once: the scheme (https, always), the host (apex or www, either is fine, but pick one), and the trailing-slash convention. Everything else is a non-canonical starting point that should reach that final form in exactly one hop.
The four-hop mistake is what you get when each of those decisions is enforced by its own rule, in sequence. A request for http://example.com/pricing goes to https://example.com/pricing (the HTTPS rule), then to https://www.example.com/pricing (the host rule), then to https://www.example.com/pricing/ (the trailing-slash rule). Three hops, each individually sensible, and the visitor waits for all of them.
The fix is not to remove any of the rules but to make each one jump straight to the final URL rather than to the next rule's input. A request arriving on port 80 for the apex should be answered with a single 301 to https://www.example.com/pricing/ — correct scheme, correct host, correct path — not with a redirect that merely fixes the scheme and leaves the rest to somebody else.
One detail decides how many rules you need. If you redirect http://example.com straight to https://www.example.com, that is one hop and it is the right shape for speed. But the browser then never sees an HTTPS response from example.com itself, so it never receives an HSTS header for that hostname — which is the subject of the next section, and the one reason you might deliberately choose two hops instead of one.
How HSTS changes the port-80 hop
HSTS is a header a browser can only learn from an HTTPS response, and it applies to exactly the hostname that sent it. A browser that has it for a host refuses to make a plaintext request to that host at all — it rewrites the URL to https internally before anything goes on the wire, which removes the port-80 hop entirely for returning visitors.
That creates a genuine tension with the single-hop advice above. Redirect http://example.com straight to https://www.example.com and the browser never gets an HTTPS response from example.com, so it never learns an HSTS policy for that name, and every future visit to the apex over plain HTTP is interceptable all over again. Redirect http://example.com to https://example.com first, let that response carry the HSTS header, and then redirect to www — and you have paid for an extra hop to buy the protection.
There are two ways out, and which one applies depends on whether you can cover the apex some other way. If the canonical host is www and you set includeSubDomains on the www policy, that does not cover the apex — includeSubDomains extends downward from the host that sent it, and the apex is above www, not below it. If instead your canonical host is the apex and the policy is set there with includeSubDomains, every subdomain including www is covered from one header and the single-hop redirect from www to the apex costs you nothing.
So: a site canonical on the apex can have both the single hop and full HSTS coverage. A site canonical on www has to choose, per hostname, between one fewer hop and an HSTS policy on the apex. The extra hop is only paid on the first visit before the policy is learned, which is usually the better trade, and this is a case where our own chain-length finding and our own HSTS finding can legitimately pull in opposite directions — the HSTS one wins.
Whichever you choose, the port-80 listener has to keep serving /.well-known/acme-challenge/ over plain HTTP without redirecting it, or certificate renewal over the HTTP-01 challenge stops working. This is the single most common cause of a certificate that silently fails to renew.
Diagnosing a chain with curl
curl -sIL follows the chain and prints the headers of every response in it, which is the whole picture rather than the destination. -I sends a HEAD request, -L follows redirects, and -s suppresses the progress meter. Start from the worst case: plain HTTP, on the non-canonical host, with a real path rather than the home page, because home-page redirects are frequently the only ones anyone tested.
Read the output as pairs: each status line with the Location that follows it. What you are looking for is the number of status lines before the final 200, any Location that starts with http:// rather than https://, any hop where the path or the query string changed when it should not have, and any 302 where you expected a 301.
Check a deep URL with a query string specifically. A rule written as a redirect to the site root rather than to the equivalent path is invisible on the home page and destroys every deep link, and a rule that drops the query string breaks every campaign URL and every shared search result.
# every hop, with status and destination
curl -sIL --max-redirs 10 http://example.com/pricing?ref=test \
| grep -iE '^HTTP/|^location:'
# how many responses before the page: 1 means no redirect at all
curl -sIL http://example.com/pricing | grep -c '^HTTP/'
# where the time goes on a single hop
curl -o /dev/null -s -w 'dns %{time_namelookup} connect %{time_connect} tls %{time_appconnect} ttfb %{time_starttransfer}\n' \
https://example.com/Fixing it: nginx, Apache and Cloudflare
The shape of the fix is the same everywhere: one rule per non-canonical starting form, each jumping straight to the final URL, preserving the path and the query string. The configuration below is a dated snapshot — verified against the current nginx, Apache and Cloudflare documentation in September 2026 — because dashboard paths in particular do drift.
In nginx, use return rather than rewrite. return ends the request immediately; rewrite re-enters location matching and is the easiest way to build a loop by accident. $request_uri contains the path and the query string together, so it preserves both without any extra work.
In Apache, put the rules in the virtual host rather than in .htaccess. .htaccess is re-read on every single request, is disabled outright on many hardened installations by AllowOverride None, and its rules run after the virtual host's — which is a large part of how these chains get long in the first place. RedirectMatch with a capture is enough for the ordinary case and does not need mod_rewrite.
On Cloudflare, as of September 2026, the documented path is the Rules Overview page, then Create rule, then Redirect Rule — Cloudflare calls the feature Single Redirects. A single rule can match on hostname and produce the full destination including the path. It is available on the Free plan, with a quota of 10 rules per zone, and it only applies to DNS records that are proxied through Cloudflare.
Two Cloudflare details are worth knowing. Its Always Use HTTPS setting, under SSL/TLS then Edge Certificates, performs its own http-to-https redirect, so leaving it on alongside a Redirect Rule that already targets https is a common way an extra hop appears. And everything Cloudflare does happens at its edge: an origin reachable directly, by IP address or through a DNS record that is not proxied, still serves whatever chain it served before, which is what our scanner measures when it can reach the origin. Cloudflare's own documentation recommends not performing redirects at the origin, because an origin redirect fighting an edge one is a classic loop — so the resolution is not to configure the same redirect twice but to make the origin's rule agree with the edge's rather than compete with it, and to make sure the origin is not publicly reachable off-proxy in the first place.
# nginx — apex on 80 and 443, plus www on 80, all land on https://www.example.com
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Never redirect the ACME challenge, or renewal breaks.
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://www.example.com$request_uri;
}
}
server {
listen 443 ssl;
server_name example.com;
# This block exists to send HSTS for the apex before handing over to www.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
return 301 https://www.example.com$request_uri;
}
# Apache — in the virtual host, not .htaccess
# <VirtualHost *:80>
# ServerName example.com
# ServerAlias www.example.com
# RedirectMatch 301 ^/(?!\.well-known/acme-challenge/)(.*)$ https://www.example.com/$1
# </VirtualHost>What redirects do to search results
A 301 is the instruction that consolidates two URLs into one in a search index: the old URL's signals are attributed to the new one and the old URL eventually stops being served in results. A 302 is the instruction not to do that, so both URLs stay in the index competing with each other, which is the actual SEO cost of using a temporary code for a permanent move.
Google states its position plainly, and it is less alarming than the folklore. Its crawlers follow up to 10 redirect hops by default, and its site-move documentation says to avoid chaining redirects, to redirect to the final destination directly, and if that is not possible to keep the chain to ideally no more than three hops and fewer than five. The reasons it gives are the ones above: chaining adds latency for users, and not all user agents support long chains. So a three-hop chain is not an indexing catastrophe; it is slower for visitors and it is untidy, and those are sufficient reasons to fix it.
When you move a site, update the existing redirect targets to point at the new final destination rather than adding a new redirect in front of the old chain. That is how a one-hop redirect becomes a five-hop chain over a couple of migrations.
Keep the redirect in place for a long time after a move. Google's guidance is to keep them at least a year, so that it can recrawl and reassign links on other sites that point at the old URLs, and to consider keeping them indefinitely from a user's point of view while updating your own links and any high-volume inbound links to point at the new URLs directly.
A redirect and a canonical link element say related but different things, and they should not disagree. A redirect moves the visitor; a canonical tag on a page that loads normally says which URL among several near-identical ones should be indexed. A page that redirects and also carries a canonical tag pointing somewhere else is giving two conflicting instructions.
Meta refresh and JavaScript redirects
A meta refresh tag in the document head, and a location assignment in JavaScript, both move the browser to another URL — but neither is an HTTP redirect, and the difference is larger than it looks.
Both require the full page to be fetched and parsed first, so they are strictly slower than a 3xx: the visitor downloads a document whose only purpose is to send them somewhere else. Neither carries a status code, so no cache and no intermediary can act on it. A meta refresh with a delay produces a visible flash of the intermediate page and breaks the browser's back button, because the history entry is real. And a JavaScript redirect does not happen at all for any client that does not execute JavaScript.
Google does process both, and its documentation is specific about how. A meta refresh with a delay of zero seconds is interpreted as a permanent redirect; one with any delay above zero is interpreted as a temporary redirect. The HTTP Refresh header is treated the same way. A JavaScript redirect is classified as permanent, but Google's own caution is worth repeating: it says to use JavaScript redirects only if you cannot do a server-side or meta refresh redirect, because rendering can fail for various reasons and in that case Google might never see the redirect at all.
So this is not the total failure it is sometimes described as. It is simply worse on every axis than the server-side alternative: slower, uncacheable, and dependent on client behaviour you do not control.
The one legitimate case is where you genuinely cannot set a response header — static hosting with no configuration surface, or a page inside a system you do not administer. Everywhere else, use a 301 or a 308.
Verifying the fix
Re-test from the worst-case starting point rather than from the URL you were working on, and test more than the home page. The specific things to confirm: one hop from each non-canonical starting form, a 301 or 308 rather than a 302, the path and the query string intact at the destination, no hop whose Location begins with http://, and a 200 at the end.
Then check the two things a chain trace alone does not show. Confirm that /.well-known/acme-challenge/ is still served over plain HTTP without a redirect, by requesting a path under it and expecting a 404 from your own server rather than a 301. And confirm the HSTS header is being sent by the hostname you intended to send it, which is not necessarily the one at the end of the chain.
Our redirect checker walks the chain one guarded hop at a time and reports each hop separately, including its status, timing and headers, which is the same information as the curl trace with the hops that changed hostname marked — because the security headers, the cookies and the certificate all belong to the final host rather than to the one you typed.
What commonly goes wrong
- A 302 on the www or HTTPS canonicalisation redirect. It is never cached, so every visitor pays the extra round trip on every visit, and search engines keep both URLs in the index instead of consolidating them.
- A redirect to the site root instead of the equivalent path. Invisible when you test the home page, and it silently destroys every deep link, every bookmark and every inbound link to an article.
- Redirecting /.well-known/acme-challenge/ to HTTPS along with everything else. Certificate renewal over the HTTP-01 challenge then fails, quietly, until the certificate expires and the site goes down.
- A proxy that terminates TLS and an origin that redirects HTTP to HTTPS, with X-Forwarded-Proto not honoured. This produces an infinite loop, and the affected URLs are unreachable for everyone.
- A 301 where the client POSTs. Browsers and many libraries turn the POST into a GET and drop the body, so the request appears to succeed while doing nothing. Use 308 for anything an API client calls.
- Cloudflare's Always Use HTTPS left on alongside a Redirect Rule that already targets https, adding a hop nobody configured and nobody can see in the origin's own files.
- Adding a new redirect in front of an existing chain during a migration rather than updating the existing rules to point at the new final destination. Two migrations done that way turn a one-hop redirect into a five-hop one.