Redirect chains and loops after a migration: how they happen and how to flatten them

old rules meet a new map, and a one-hop redirect becomes two or three. how to find every chain and loop, and how to flatten them before launch.

by Max Lorenz, Goodaim · updated

a chain, flattened
  • /produkte/zange (2019 rule)hop 1
    /shop/zange
  • /shop/zange (2026 map)hop 2
    /products/pliers
  • /produkte/zange1 hop
    /products/pliers

SHORT ANSWER

A redirect chain is an old url that redirects to a url that redirects again before a page answers; a loop is a chain that returns to a url it already passed and never answers at all. After a relaunch, chains appear mostly where the previous migration’s rules meet the new map: A → B from last time, B → C now. Google follows up to 10 hops but advises redirecting to the final destination directly. The fix is always the same: rewrite every rule so its source points straight at the final url, and check that the final url answers 200.

Chains, loops, and what Google does with them

chain — /a → /b → /c → 200. The page is reached, one or more hops later than necessary.

loop — /a → /b → /a. The page is never reached; browsers give up with a too-many-redirects error.

Google’s site move guide is direct about chains: “Avoid chaining redirects. While Googlebot can follow up to 10 hops in a ‘chain’ of multiple redirects (for example, Page 1 > Page 2 > Page 3), we advise redirecting to the final destination directly.” The same guide asks to keep chains short — ideally no more than three hops and fewer than five. Its status code documentation adds that the 10 hops are a default for Google’s crawlers and that other products’ crawlers may have different limits.

Loops and over-long chains show up in Search Console’s page indexing report as redirect error, which covers “a redirect chain that was too long”, “a redirect loop”, a redirect url that exceeded the maximum url length, and a bad or empty url in the chain. A chain of two or three hops does not appear there at all — which is why chains survive for years unless you look for them.

Beyond Google: every hop is another round trip for every visitor who arrives through an old link, and other crawlers and link checkers may give up sooner. A one-hop map costs nothing extra to write; a chain is always avoidable.

How chains arise in a relaunch

causewhat it looks likewhere to fix it
the previous relaunch's rules/old-2019 → /old-2023 (kept), /old-2023 → /new (new map)merge both rule sets and flatten
protocol and host upgradeshttp://example.com/a → https://example.com/a → https://www.example.com/a → /bone rule that lands on the final https url
trailing-slash normalisation/a → /a/ (platform) → /b (your rule)write sources in the form the platform serves, or match both
targets renamed after mapping/a → /pricing, then /pricing → /plans added laterrebuild or re-check the map after content changes
cms slug redirectsthe cms redirects an edited slug, and your map redirects to the old slugpoint the map at the current slug
a wildcard plus exact rules/blog/(.*) → /magazine/%1, and /magazine/x → /magazine/ygive the exact row the final target, before the wildcard

The first cause is the big one, and it is easy to miss on a platform change. When a site moves from one system to another, the old server’s redirect rules do not come along by themselves. Their sources — urls that were already retired once — still receive links and need a rule on the new platform. Re-importing the old file unchanged creates chains wherever its targets are now moving again; dropping it creates 404s. The right move is to merge it into the new map and flatten.

EXAMPLE

the 2019 relaunch redirected /produkte/zange to /shop/zange. The 2026 map moves /shop/zange to /products/pliers. Both rules end up in the new site’s import, and every link to the 2019 url now takes two hops. Flattened, the old rule reads /produkte/zange → /products/pliers.

How loops arise

  • two rules pointing at each other. /a → /b from one rule set, /b → /a from another — typical when a page was moved and later moved back.
  • a wildcard that matches its own target. /shop/(.*) → /shop/products/%1: the target starts with /shop/, so it matches the rule again and grows with every hop until the browser gives up. Anchor the pattern or choose a target outside it.
  • normalisation rules fighting. One layer adds a trailing slash, another removes it; or a cdn forces www while the origin forces the bare domain.
  • a redirect to a page that redirects back. A new page that itself has a redirect to the old url, left over from staging.

Finding chains before and after launch

Three ways, from cheapest to most thorough:

in the spreadsheet — with sources in column A and targets in column B, =COUNTIF(A:A, B2)>0 flags every target that is itself a source. Each flagged row is a chain, and a pair of rows flagging each other is a loop. Works before launch, on the map itself — see the redirect map template.

with curl, against the live site — curl -s -o /dev/null -L -w '%{num_redirects} %{url_effective}' URL prints how many redirects were followed and where the request ended. More than one hop is a chain; curl gives up after its --max-redirs limit (50 by default with -L) on a loop. A loop over a whole url list is in how to check redirects after launch.

with a crawler in list mode — Screaming Frog’s redirect audit tutorial: list mode, the old urls uploaded, “Always Follow Redirects” switched on, then the All Redirects report, which has columns for the number of redirects, whether the chain loops, the final status code and the final address.

Test the http and the non-preferred host variants of a few urls too. A map that is flat for https://www.example.com/a can still take three hops for http://example.com/a.

How to flatten them

  • merge every rule set into one table — the new map, the old redirect file, the platform’s existing rules, any server rules. One row per source.
  • normalise the spelling of both columns: same host, case and trailing-slash convention. A chain hidden behind /b vs /b/ is still a chain.
  • follow each target through the table until it reaches a url that is not itself a source. That url is the final target; write it into the row.
  • stop on a repeat. If the walk reaches a url it already visited, the rows form a loop: decide which url is the real destination and fix the rule that points back.
  • drop identity rows — a source that now points at itself needs no rule, and some importers reject one.
  • check the final targets answer 200 on the new site. A flat rule to a 404 is no better than a chain.

For a few dozen rows the spreadsheet formula and a careful afternoon are enough. For more, a short script does the walk. This one reads a two-column csv of paths, prints the flattened rules with the number of hops each one used to take, and reports loops:

import csv, sys

# redirects.csv: header row, then from,to — paths spelled the same way on both sides
rules = {}
with open(sys.argv[1], newline="") as f:
    reader = csv.reader(f)
    next(reader)
    for row in reader:
        rules[row[0].strip()] = row[1].strip()

out = csv.writer(sys.stdout)
out.writerow(["from", "to", "hops_before"])
for source in rules:
    seen = [source]
    target = rules[source]
    while target in rules:
        if target in seen:
            sys.stderr.write("loop: " + " -> ".join(seen + [target]) + "\n")
            break
        seen.append(target)
        target = rules[target]
    else:
        if target != source:
            out.writerow([source, target, len(seen)])

Run it as python3 flatten.py redirects.csv > flat.csv. It handles exact rules only; wildcard rows need to be expanded into the urls they cover first, or checked by hand against the exact rows below them.

Where Silentfrog helps, and where it does not

Silentfrog matches old pages only against pages of the new site that answered 200 during the crawl, so the targets it suggests are final at the time of the crawl, not urls that redirect again. A directory rule whose targets do not exist on the new site sends those rows to needs review instead of exporting them silently (whole directories at once).

redirect a directory

redirect a whole directory

map every crawled page below a directory at once — optionally as a single webflow wildcard rule that also covers urls the crawl missed.

→

3 pages · 3 targets exist on the new site · + wildcard /blog/(.*) → /magazin/%1

  • /blog/relaunch-checkliste → /magazin/relaunch-checkliste
  • /blog/301-vs-302 → /magazin/301-vs-302
  • /blog/ladezeit → /magazin/ladezeit

the directory rule form mapping /blog to /magazin with sub-paths kept, exported as a wildcard, and a live preview of how many pages it affects.

a directory rule with its live preview, including the targets that do not exist on the new site.

What it does not do, so you know where the chain risk still sits:

  • an old url that already redirected during the crawl does not get a row of its own — the crawler follows it and maps the page it lands on. The previous relaunch's sources therefore have to be merged and flattened as described above.
  • it does not know the redirect rules already stored in your platform or server, so a chain formed by an existing rule plus the new map is caught by the merge step, not by the tool.
  • it builds the map; it does not test the live site after launch. That check is a curl loop or a crawler in list mode.

In short

  • A chain reaches the page late; a loop never reaches it. Google follows up to 10 hops but wants one.
  • Most relaunch chains come from the previous migration's rules meeting the new map.
  • Protocol, host and trailing-slash rules add hops of their own — test the http and bare-domain variants too.
  • Find chains with =COUNTIF(A:A, B2)>0 before launch and with curl or a list-mode crawl after.
  • Flatten: merge all rule sets, follow every target to its end, write the final url, check it answers 200.

For the new half of the map, Silentfrog crawls both sites and matches every old url to a page that answers 200 — the first 50 pages are free.