Flattening Multi-Hop Redirect Chains Into Single Hops

Problem Statement

After several migrations, a single old URL can pass through two, three, or more redirects before reaching its final destination — each hop adding latency and burning crawl budget. You need to detect every chain and rewrite each starting URL so it points directly at the final 200 target in one 301. This page sits under Redirect Chain Elimination.

Chain before and after flattening A three-hop redirect chain on top, collapsed below into a single 301 from the original URL straight to the final destination. Flatten Multi-Hop Chain to One Hop Before /old-page /interim-page /final-page After /old-page /final-page single 301
Each original URL is rewritten to point directly at the final destination, removing every interim hop.

When to Use This Approach

  • A crawl or curl -IL shows more than one redirect before a 200.
  • The site has been migrated more than once, layering redirect rules over time.
  • Googlebot reports “Redirect error” or “Page with redirect” on URLs that should resolve cleanly.
  • Latency on entry URLs is inflated by sequential hops, hurting Time to First Byte.
  • A framework redirect and an old server rule both fire on the same path.

Step-by-Step Instructions

1. Detect the Chain with curl

curl -IL follows redirects and prints every hop. Count the HTTP status lines — anything more than one redirect before the final 200 is a chain to flatten.

# Follow all hops and show only status and Location for each step
curl -sIL https://www.example.com/old-page | grep -iE '^HTTP|^location'

curl -IL is the right detector because it observes the composed behaviour of every layer, which no configuration file describes. Run it across a sample drawn from each route class rather than a handful of URLs somebody picked, and record the hop count alongside the terminal destination — the hop count is what tells you a chain exists, and the terminal destination is what you need in order to flatten it. Automate the sweep so it can be re-run after every change, because a single new pattern can re-chain routes that were previously direct.

Pay particular attention to routes that pass through more than one owner. A chain formed entirely within your server config is visible in that config to anyone who reads it carefully; a chain formed by a server rule composing with a CMS slug handler or a CDN rule appears in neither, and is only observable at runtime.

2. Extract the Final Destination for Each URL

For every entry URL in your list, capture the last URL curl actually lands on. That final URL becomes the single destination for the flattened rule.

# Print "start_url final_url" for each line in urls.txt
while read u; do
  printf '%s %s\n' "$u" "$(curl -sIL -o /dev/null -w '%{url_effective}' "$u")"
done < urls.txt > flattened-map.txt

3. Build a Rewrite Map Pointing Start Directly at Final

Convert flattened-map.txt into server rules where each original URL 301s straight to the final target — skipping every intermediate hop.

# Nginx map: each original path goes directly to the final destination
map $request_uri $flat_target {
    /old-page        /final-page;
    /interim-page    /final-page;
}
server {
    if ($flat_target) { return 301 $flat_target; }
}
# Apache equivalent — original and any interim path both jump to the final target
RewriteRule ^/old-page/?$     /final-page [R=301,L]
RewriteRule ^/interim-page/?$ /final-page [R=301,L]

Build the flattened map from the resolved data, not by editing the existing rules. The distinction matters because editing rules one at a time reintroduces exactly the inconsistency you are removing — two people editing different parts of the same chain produce a map that is flat in places and not in others, with no way to tell which. Resolve the whole mapping in one pass, regenerate the config from it, and diff the output against the previous version so the change is reviewable.

The resolution itself must detect cycles and stop rather than truncating at an arbitrary depth. A loop in the mapping is a genuine defect requiring a human decision about which URL is canonical, and quietly resolving it to whichever URL the traversal reached last will differ from how the server resolves it at runtime.

4. Remove the Now-Redundant Interim Rules

Once both the original and interim paths point at the final target, delete the old chained rules so a future request cannot re-enter the chain through a stale entry point.

# Confirm no rule still points to an interim hop before deleting it
grep -n 'interim-page' /etc/nginx/conf.d/redirects.conf

Flattening is a resolution over the map, not a rewrite of individual rules — and framing it that way is what makes it tractable at estate scale.

Resolving a chain to its terminal destination A three-hop chain from an original URL through two intermediate destinations, resolved so the first rule points directly at the terminal URL and the interim rules are retained only for their own inbound traffic. Resolve in the data, then emit one rule per source /a /b /c /d — terminal before: three hops from /a after: /a → /d, /b → /d, /c → /d — every source one hop from its terminal Keep the interim rules: /b and /c have their own inbound links, and deleting them turns those into 404s. What changes is their destination, not their existence.
Flattening does not mean fewer rules — it means every rule points at the end of the chain rather than at the next link.

Worked Example

Before flattening, /old-page chains through two hops:

$ curl -sIL https://www.example.com/old-page | grep -iE '^HTTP|^location'
HTTP/2 301
location: https://www.example.com/interim-page
HTTP/2 301
location: https://www.example.com/final-page
HTTP/2 200

After adding /old-page → /final-page and /interim-page → /final-page to the map:

$ curl -sIL https://www.example.com/old-page | grep -iE '^HTTP|^location'
HTTP/2 301
location: https://www.example.com/final-page
HTTP/2 200

One hop instead of two, with the interim URL also resolving directly so no entry point re-enters the chain.

Removing the interim rules is the mistake this procedure most often invites, and it converts a performance problem into a correctness one.

Why interim rules are retained after flattening The intermediate URLs in a chain carry their own inbound links and traffic, so deleting their rules after flattening turns working URLs into 404s. The middle of a chain is somebody's bookmark Retarget the interim rules /b → /d and /c → /d kept every entry point still resolves correct Delete the interim rules only /a → /d remains /b and /c now 404 breaks live traffic Interim URLs were real destinations once, so they accumulated links, bookmarks and integrations of their own. Retire them only when the logs show traffic to them has genuinely decayed.
A rule is only redundant if nothing requests its source — and the way to know that is the access log, not the chain diagram.

Re-run the detection sweep after deploying, not just before. Flattening changes the map, and the map is compiled into config that is then composed with whatever other layers are in play — so the only evidence that the estate is actually flat is a runtime sweep against the deployed stack showing every sampled source resolving in one hop. Treat a single remaining two-hop route as a finding rather than as rounding error, because chains propagate: today’s two-hop route becomes tomorrow’s three-hop one the next time anything upstream changes.

Verification

  • Re-run curl -sIL on every flattened URL and confirm exactly one 301 before the 200.
  • Batch-check the whole list: count status lines per URL and flag any with more than two HTTP lines.
  • After deploy, watch Google Search Console for “Page with redirect” counts dropping over subsequent crawls.

FAQ

How many hops are acceptable? One. Search engines tolerate several hops, but every extra redirect adds latency and wastes crawl budget. Flatten every chain so each entry URL reaches its final destination in a single 301.

Why redirect the interim URL too? Because the interim URL is often still linked or indexed. If you only fix the original, a request that enters at the interim path still resolves in one hop — but leaving the interim chained would let it re-form a multi-hop path.

Can flattening be automated safely? Yes, and it should be — the resolution is a graph traversal, which is exactly the kind of thing done better by code than by a person reading rules. The safety condition is that the traversal detects cycles and refuses to emit anything if it finds one, rather than terminating at an arbitrary depth. A cycle in the mapping is a real defect that needs a human decision about which URL is canonical, and silently truncating it produces a rule set that resolves the loop differently from how the server will.

What if a chain crosses layers I do not control? Trace it at runtime and negotiate ownership. A chain formed partly by your server rules and partly by a CDN configuration owned by another team, or by a CMS plugin storing rules in a database, cannot be resolved from your side alone — and the first step is establishing which layer should own the path at all. Until that is agreed, the best available mitigation is to make your own layer point at the terminal destination so your contribution to the chain is one hop rather than two.

How do I decide when an interim rule can finally be deleted? From the access logs, not from the age of the rule. Query the request count for that source over the last quarter; if it is effectively zero and the URL has no inbound links worth preserving, it can go. Anything still receiving traffic is still doing its job, however old it is — some URLs retired years ago continue to receive steady traffic from printed material, email archives, or an integration nobody remembers building. Deleting on a schedule rather than on evidence is how migrations create 404s that nobody predicted.

Does flattening change how much equity reaches the destination? It improves it, though the size of the effect is smaller than the crawl-budget argument and harder to measure. The stronger practical reasons are latency — every hop is a full round trip on every request, including on mobile connections where that is expensive — and crawler behaviour, since crawlers give up after a bounded number of hops and spend budget walking chains that could have been one step. Treat equity as the third reason rather than the first.

Related

← Back to Redirect Chain Elimination