Managing redirects at scale means keeping every redirect in one place, using permanent redirects for permanent moves, pointing each old URL straight to its final destination with no chains or loops, and testing the whole list automatically after every change. Large sites accumulate thousands of redirects from migrations, renamed products and restructures; unmanaged, they slow pages, waste crawling and lose the value of old links. A single source of truth and a checker script prevent most problems.
Choose the right status code
| Code | Meaning | Use for |
|---|---|---|
| 301 or 308 | Permanent | Moved pages, migrations, merged content |
| 302 or 307 | Temporary | Short-term changes, such as a page under maintenance |
Google treats permanent redirects as a strong signal that the target should be canonical. Temporary redirects used for permanent moves can leave the old URL in search longer.
Rules for a clean redirect set
- One source of truth: redirects live in one system, such as the web server, CDN or application, not spread across all three.
- No chains: every old URL points directly to the final URL. When a page moves again, update older redirects to the new target.
- No loops: A to B to A breaks the page entirely.
- Relevant targets: redirect to the closest equivalent page; mass redirects to the home page are treated as soft 404s.
- Patterns where possible: one rule for a whole old folder is easier to maintain than thousands of lines.
- Keep them: Google recommends keeping redirects for at least a year after a move; there is little reason to remove them.
Large lists in Nginx
For thousands of one-to-one redirects, a map is faster and cleaner than many separate rules:
# in the http block map $request_uri $redirect_target { include /etc/nginx/redirects.map; # lines like: /old-page/ /new-page/; } # in the server block if ($redirect_target) { return 301 $redirect_target; }
On Cloudflare, Bulk Redirects handles large lists; in Next.js, the redirects function in next.config.js; in WordPress, a redirect plugin or server rules. See edge SEO with Cloudflare Workers.
Test the whole list
This Python script follows each redirect hop by hop without automatically following redirects, and reports chains, loops, temporary redirects, broken final destinations and redirects that end somewhere unexpected.
#!/usr/bin/env python3
"""Check redirects: follow each URL hop by hop and report chains, loops, temporary redirects
and wrong or broken destinations.
Input: redirects.csv with columns source,expected (absolute URLs).
Usage: python3 redirect_check.py redirects.csv"""
import csv, sys, urllib.request, urllib.error
from urllib.parse import urljoin
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
opener = urllib.request.build_opener(NoRedirect)
def hop(url):
try:
r = opener.open(urllib.request.Request(url, method="HEAD"), timeout=15)
return r.status, None
except urllib.error.HTTPError as e:
return e.code, e.headers.get("Location")
for row in csv.DictReader(open(sys.argv[1])):
url, seen, chain, issues = row["source"], set(), [], []
while True:
status, loc = hop(url)
chain.append(f"{status}")
if status in (301, 302, 303, 307, 308) and loc:
if status in (302, 303, 307): issues.append("temporary redirect")
nxt = urljoin(url, loc)
if nxt in seen: issues.append("LOOP"); break
seen.add(url); url = nxt
if len(chain) > 10: issues.append("too many hops"); break
else:
break
hops = len(chain) - 1
if hops > 1: issues.append(f"chain of {hops} hops")
if chain[-1] != "200" and "LOOP" not in issues: issues.append(f"final status {chain[-1]}")
if row.get("expected") and url != row["expected"] and "LOOP" not in issues: issues.append(f"ends at {url}")
print(("FAIL " if issues else "ok ") + row["source"] + (" -> " + "; ".join(dict.fromkeys(issues)) if issues else ""))
We tested it against a small local server with one clean redirect, a three-hop chain, a loop and a redirect-less missing page. It reported:
ok /ok-old/
FAIL /a/ -> chain of 3 hops
FAIL /loop1/ -> LOOP
FAIL /gone/ -> final status 404; ends at /gone/
Run it after every deployment that changes redirects, and after migrations. Some servers treat HEAD requests differently from GET; if results look wrong, switch the method.
Keep redirects documented
- Record why each group of redirects exists and when it was added.
- Update internal links to point to final URLs, so redirects serve only old external links and bookmarks.
- Review 404 logs monthly for old URLs that still receive visits and need a redirect.
Frequently asked questions
Do redirect chains lose ranking value?
Google follows several hops, but chains slow users and crawlers and increase the risk of breaks. Pointing directly to the final URL is simpler and safer.
How many redirects are too many?
There is no hard limit. Performance depends on how they are implemented; maps and lookup tables handle large lists efficiently.
Should we redirect deleted pages?
Only when a genuinely equivalent page exists. Otherwise return 404 or 410. See 404s and soft 404s at scale.
Sources
- Google Search Central, “Redirects and Google Search”
- Google Search Central, “Site moves with URL changes”
- Nginx documentation, ngx_http_map_module