Bulk redirect check after a site migration
After a domain move, an HTTPS switch or a new CMS, every old URL should reach a live, indexable page in one permanent redirect. How to check a whole list for free, what a plain redirect checker cannot see, and a checklist you can reuse.
Script tested, run made and sources checked on 28 October 2026.
A migration usually comes with a redirect map: a spreadsheet of old URLs and where each one should go. Once the new site is live you need to prove the map works, for every row, not just the ten you clicked. That means a bulk check: feed in the list of old URLs, get back the status code of every hop and the final address. This guide covers the free ways to do that first, because for status codes and hops alone they are enough. Then it covers the problems a redirect check does not catch, and when a combined check is worth paying for.
Disclosure: Quarrywise, the publisher of this site, makes and sells the website health check described near the end. The free methods need nothing from us.
What a good migration redirect looks like
For each old URL, four things should be true:
- One hop. The old URL redirects straight to its final address. Google's site move guide asks you to avoid chains and redirect "to the final destination directly".
- Permanent. The redirect is a 301 or 308. Google uses these as a signal that the target should be the canonical URL. A 302, 303 or 307 is not used as that signal, and the old URL may stay the one shown in results.
- A live target. The final address answers 200. Google does not use content from URLs that answer with a 4xx code, and drops previously indexed ones.
- An indexable, relevant target. The final page is not set to
noindex, is not blocked by robots.txt, has a canonical that points to itself, and is the closest match for the old page. Google advises against sending many old URLs to one irrelevant page, such as the new home page.
The common ways it goes wrong:
| Problem | What you see | Why it matters |
|---|---|---|
| Chain | http to https, then a second hop to add a trailing slash or the new path | Slower first visit and more crawling. Googlebot follows up to 10 hops, but every hop is a place to break. |
| Temporary redirect | 302 or 307 where the move is final | The old URL can stay the one Google shows. |
| Broken target | The redirect works but the new URL answers 404 or 500 | The page drops out of the index; links to the old URL lose their value. |
| Loop | A redirects to B, B back to A | Nobody, visitor or crawler, reaches a page. |
| Hidden blocker on the target | 200 OK, but the page says noindex or its canonical points elsewhere |
The redirect looks perfect in a redirect checker and the page still will not rank. |
Free option 1: a curl loop over your URL list
If you have a terminal, this costs nothing. The script below reads old URLs from a text file, one per line, and writes one CSV row per URL: the first status code, where it points, the number of hops, the final status, the final URL and a short verdict. It makes two requests per URL (one that stops at the first answer, one that follows up to 10 redirects, like Googlebot) and waits one second between URLs.
To use it:
- Save the script as
check_redirects.sh. - Put the old URLs in
old-urls.txt, one per line. A file saved on Windows works too: the script strips the extra line ending. - Run
bash check_redirects.sh old-urls.txt > redirects.csv. It runs on macOS and Linux, and on Windows in Git Bash or WSL. We tested it with curl 8.17.0 in Git Bash on Windows.
#!/usr/bin/env bash
# For each old URL in a text file: the first status code, where it points, the number of
# redirect hops, the final status and the final URL, plus a short verdict. Output is CSV.
# Usage: bash check_redirects.sh old-urls.txt > redirects.csv
echo "old_url,first_status,first_location,hops,final_status,final_url,verdict"
while IFS= read -r url || [ -n "$url" ]; do
url="${url%$'\r'}" # files saved on Windows end lines with \r
[ -z "$url" ] && continue
# 1. One request without following redirects: the first answer and its Location.
first=$(curl -s -o /dev/null --max-time 20 -w '%{http_code} %{redirect_url}' "$url")
first_status="${first%% *}"
first_location="${first#* }"
# 2. One request that follows up to 10 redirects, like Googlebot: hops and final answer.
final=$(curl -s -o /dev/null -L --max-redirs 10 --max-time 30 \
-w '%{http_code} %{num_redirects} %{url_effective}' "$url")
read -r final_status hops final_url <<< "$final"
if [ "$final_status" = "000" ]; then verdict="no answer (DNS, TLS or timeout)"
elif [[ "$final_status" == 3* ]]; then verdict="loop or more than 10 hops"
elif [ "$final_status" != "200" ]; then verdict="ends on $final_status"
elif [ "$hops" -eq 0 ]; then verdict="no redirect"
elif [ "$hops" -gt 1 ]; then verdict="chain of $hops hops"
elif [ "$first_status" != "301" ] && [ "$first_status" != "308" ]; then verdict="temporary ($first_status)"
else verdict="ok"
fi
echo "\"$url\",$first_status,\"$first_location\",$hops,$final_status,\"$final_url\",\"$verdict\""
sleep 1 # be polite: about one URL per second
done < "$1"
We ran it on eight URLs chosen to hit every verdict: old addresses on our own site, a temporary redirect and a chain of 12 redirects from the public test service httpbin.org, and a domain that does not exist. It took 14 seconds and wrote:
old_url,first_status,first_location,hops,final_status,final_url,verdict
"http://quarrywise.com/",301,"https://quarrywise.com/",1,200,"https://quarrywise.com/","ok"
"http://www.quarrywise.com/",301,"https://www.quarrywise.com/",1,200,"https://www.quarrywise.com/","ok"
"http://quarrywise.com/guides",301,"https://quarrywise.com/guides",2,200,"https://quarrywise.com/guides/","chain of 2 hops"
"http://quarrywise.com/old-page",301,"https://quarrywise.com/old-page",1,404,"https://quarrywise.com/old-page","ends on 404"
"https://quarrywise.com/",200,"",0,200,"https://quarrywise.com/","no redirect"
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fexample.com%2F&status_code=302",302,"https://example.com/",1,200,"https://example.com/","temporary (302)"
"https://httpbin.org/redirect/12",302,"https://httpbin.org/relative-redirect/11",10,302,"https://httpbin.org/relative-redirect/2","loop or more than 10 hops"
"https://old.example.invalid/page",000,"",0,000,"https://old.example.invalid/page","no answer (DNS, TLS or timeout)"
Open the CSV in a spreadsheet and filter on every verdict that is not ok. To compare
with your redirect map, look up each old URL's final_url against the target in the map:
a redirect that works but goes to the wrong page is still wrong.
What to know about this route:
- Speed. At about 1.7 seconds per URL in our test, 1,000 old URLs take roughly half an hour. You can lower the pause, but keep the load on the server in mind.
- Hops in between. The verdict reads the status of the first hop only. In a chain, a 302 in the middle is not named, but the chain itself is flagged.
- Some servers treat curl differently. A firewall or bot protection can answer
curl with 403 while browsers get the page. Add
-A "your user agent"to both curl calls if that happens. - Status codes only. The second row above says
ok. That is not the whole story, and the section on what a redirect checker misses shows why.
Free option 2: a desktop crawler in list mode
SEO crawlers have a list mode for exactly this job: you upload the old URLs instead of crawling from a start page. Screaming Frog's SEO Spider tutorial on auditing redirects describes the steps: switch to list mode, upload the old URLs, tick "Always Follow Redirects" in the advanced configuration (list mode otherwise stops at the first answer), run the crawl, and export the "All Redirects" report, which shows the start URL, the final URL and every hop for each uploaded URL.
Limits and price, checked on 28 October 2026:
- The free version crawls up to 500 URLs. Screaming Frog notes that list mode is also limited to 500 URLs without a licence.
- A licence costs GBP 199 per year, with no crawl limit apart from your machine's memory and storage.
Because a crawler loads the final pages too, it also covers the target-page checks in the next section. Screaming Frog's pricing page lists page titles and meta data, and meta robots and directives, in the free version, so for up to 500 old URLs the free crawler can check redirects and target pages in one pass. We did not test the free version for this guide.
A third route: small hosted redirect checkers. On 28 October 2026 a search of the Apify Store for "redirect checker" listed several pay-per-URL tools, most priced from USD 0.0001 to USD 0.0015 per URL (a few charge more, up to USD 0.02), so USD 0.10 to USD 1.50 for 1,000 URLs with most of them (some add a small fee per run). That is a fraction of our price below. We did not test them. If you only need status codes and hops and do not want to run a script, they are the cheapest option.
The checks a redirect checker misses on the target page
The curl script and the hosted redirect checkers answer one question: where does the old URL end up, and with what status? A migration can pass that test and still lose rankings, because the problem sits on the new page. A crawler in list mode loads the target pages and can catch most of these; a status code check cannot:
noindexon the new page. Staging sites are often set tonoindex, by a robots meta tag or anX-Robots-Tagheader, and the setting sometimes survives the launch.- robots.txt blocks the new URLs. The same staging leftover, in robots.txt.
- The canonical points elsewhere. A template that still points every canonical at the old domain, the staging host or the home page.
- Missing or duplicate titles. A new template that gives a whole section the same title, or none.
- Soft redirects. A meta refresh or a JavaScript redirect instead of a server redirect. A status code check sees a 200 page.
- Many old URLs on one page. Hundreds of old product URLs that all land on the home page or a category page. Every row says 200, and Google's site move guide advises against it.
A real example from our own site. In the CSV above, http://www.quarrywise.com/ gets
the verdict ok: one 301 to https://www.quarrywise.com/, which answers 200.
But that page is a copy of our home page served on the www host, with an
X-Robots-Tag: noindex header and a canonical pointing to
https://quarrywise.com/. For our www host that is a deliberate stopgap until www redirects to the apex domain. For
a migrated product page, the same pattern would mean the page never ranks, while the redirect
report shows nothing wrong.
You can spot-check a few targets by hand. The first command shows each hop's status and any
X-Robots-Tag header (it uses HEAD requests, which a few servers answer differently from
GET). The second shows the robots meta tag and the canonical of the final page:
curl -sIL http://www.quarrywise.com/ | grep -i '^HTTP\|^x-robots-tag'
curl -sL http://www.quarrywise.com/ | grep -io '<meta[^>]*name="robots"[^>]*>\|<link[^>]*rel="canonical"[^>]*>'
HTTP/1.1 301 Moved Permanently
HTTP/1.1 200 OK
x-robots-tag: noindex
<link rel="canonical" href="https://quarrywise.com/">
That works for ten pages. For a few hundred, you want the same checks in the same pass as the redirect check, which is what the crawler route or the tool below gives you.
A migration checklist you can reuse
Based on Google's guide to site moves with URL changes, plus the checks above:
Before launch
- Collect every old URL: the old sitemaps, your analytics landing pages, Search Console's top pages and links report, and server logs. Include images and other files, as Google's guide suggests.
- Map each old URL to its closest new page. Do not send unrelated pages to the home page.
- Make every redirect a server-side 301 or 308, straight to the final address (https, the right host, the trailing slash your site uses).
- On the new pages: a self-referencing canonical, no
noindex, hreflang pointing at the new URLs, and a robots.txt that does not block them. Remove the staging settings.
On launch day
- Run the bulk check on the full old-URL list. Fix every row that is not one permanent hop to a 200 page that matches the map.
- Check the target pages for
noindex, robots.txt blocks, canonicals and titles. - Submit the new sitemap in Search Console, and keep the old sitemap for a while so Google recrawls the old URLs and sees the redirects. For a move to a new domain, use Search Console's Change of Address tool (not for HTTPS, www or path-only changes).
After launch
- Run the same check again after a week, and after every deploy that touches routing.
- Watch Search Console's indexing and performance reports. Google says a move can take "a few weeks or more" for a medium-sized site.
- Keep the redirects for at least a year; Google suggests as long as possible.
Redirects and SEO checks in one pass: our Actor on Apify
Our Website Health Check: Lighthouse, SEO, Tech Stack on the Apify Store follows up to 5 redirects of every address you give it, and then checks the page it ends on. For each old URL you get one row with:
final_url,status_code,redirect_countandredirect_chain(every hop with its status);- findings on the redirect itself: a chain of two or more hops, a temporary (302 or 307) redirect, a meta refresh;
- findings on the target page:
noindexin the robots meta tag or theX-Robots-Tagheader, a robots.txt rule that blocks Googlebot, a canonical that points to another page, is missing or appears twice, a missing, short or long title, a title or meta description shared with other checked pages of the same site, and the other SEO checks of the tool (48 on the page source and headers), each with a fix in plain English; - a
REPORTrecord with the whole run as Markdown, which you can paste into a client ticket or email.
When is it worth paying for? For status codes alone, the free script or a cheaper redirect checker does the job. For target-page checks on up to 500 URLs, the free version of a desktop crawler in list mode covers them too. Our tool fits when the list is longer than 500 URLs and you have no crawler licence, when you do not want to install a desktop app, or when you want the check through an API, on a schedule or as JSON.
A real run
We ran it on 28 October 2026 (run C3HQNDtTpy8KYQYtm) with four old-style addresses of
our own site in quick mode, which reads the HTML and headers without a browser:
{
"urls": [
"http://quarrywise.com/",
"http://www.quarrywise.com/",
"http://quarrywise.com/guides",
"http://quarrywise.com/old-page"
],
"mode": "quick",
"maxPagesPerSite": 1
}
maxPagesPerSite 1 means only the addresses you list are checked, with no extra pages
from the sitemap. The run took 35 seconds on Apify and returned four rows:
| Old URL | Redirect statuses | Final status | Main findings |
|---|---|---|---|
http://quarrywise.com/ |
301 | 200 | none on indexing (2 low findings: Open Graph, long meta description) |
http://www.quarrywise.com/ |
301 | 200 | noindex (critical), canonical points to another page |
http://quarrywise.com/guides |
301, 308 | 200 | redirect-chain (medium) |
http://quarrywise.com/old-page |
301 | 404 | free error row http-404 |
The www row, shortened (the full row also lists technologies and third-party scripts):
{
"url": "http://www.quarrywise.com/",
"final_url": "https://www.quarrywise.com/",
"status_code": 200,
"redirect_count": 1,
"redirect_chain": [{ "url": "http://www.quarrywise.com/", "status": 301 }],
"title": "Quarrywise: company data from public sources",
"canonical": "https://quarrywise.com/",
"seo_score": 61,
"issue_counts": { "critical": 1, "high": 0, "medium": 0, "low": 3, "info": 0 },
"issues": [
{
"id": "noindex",
"severity": "critical",
"title": "The page tells search engines not to index it",
"evidence": "X-Robots-Tag header: \"noindex\"",
"developer_fix": "Ask your developer to remove \"noindex\" from the robots meta tag or the X-Robots-Tag header for this URL."
},
{
"id": "canonical-other-page",
"severity": "low",
"title": "The canonical URL points to a different page",
"evidence": "canonical points to quarrywise.com/; page is www.quarrywise.com/"
}
],
"error": null
}
The chain and the broken target, from the same run:
{
"url": "http://quarrywise.com/guides",
"final_url": "https://quarrywise.com/guides/",
"status_code": 200,
"redirect_count": 2,
"redirect_chain": [
{ "url": "http://quarrywise.com/guides", "status": 301 },
{ "url": "https://quarrywise.com/guides", "status": 308 }
]
}
{
"url": "http://quarrywise.com/old-page",
"final_url": "https://quarrywise.com/old-page",
"status_code": 404,
"redirect_chain": [{ "url": "http://quarrywise.com/old-page", "status": 301 }],
"error": "http-404",
"error_detail": "The page answered HTTP 404."
}
And the start of the www page's section in the REPORT record:
## https://www.quarrywise.com/
Status 200, 1 redirect(s) from http://www.quarrywise.com/. SEO check score: 61/100.
### Findings (highest impact first)
- **critical** The page tells search engines not to index it: X-Robots-Tag header: "noindex"
- Fix: This page will not appear in Google. If you want it found, remove the "noindex" setting (often a "hide from search engines" switch in your site settings).
You can open the same kind of check, ready to run, as the redirect checker example task on Apify: replace its URLs with your own. Results download from the dataset as JSON, CSV or Excel.
Price and worked examples
Price when this guide was written: pay per event, USD 0.008 per page checked, plus Apify's small start fee per run (USD 0.00005 per GB of memory; quick mode runs with 1 GB by default, so USD 0.00005). Quick mode runs no Lighthouse, so there are no other charges. Free: an old URL that ends on an HTTP error (like the 404 above), a network error, a robots.txt refusal or a challenge page, and an old URL that leads to a page already checked in the same run. The Store page always shows the current price.
- The run above: 3 pages checked x USD 0.008 = USD 0.024, the 404 row free, plus USD 0.00005 start fee: about USD 0.024.
- 1,000 old URLs, each to its own page: at most 200 addresses per run, so 5 runs. 1,000 x USD 0.008 = USD 8.00, plus 5 x USD 0.00005 = USD 0.00025 in start fees: about USD 8.00.
- 1,000 old URLs where 300 end on a 404 and 200 land on a page another old URL already reached in the same run: those 500 rows are free, so 500 x USD 0.008 = USD 4.00, plus the start fees. A duplicate is free only within one run, so sort the list by target before you split it into runs of 200; old URLs that share a target then stay in the same run.
- For comparison: the curl script costs nothing; most of the hosted redirect checkers above cost USD 0.10 to USD 1.50 per 1,000 URLs; Screaming Frog is free up to 500 URLs and GBP 199 a year beyond that.
Set a maximum charge for the run on Apify, and the run stops cleanly at that amount and says so
in its OUTPUT record.
How long it takes: the tool requests about one URL per second per host, and each hop is a request, so an old URL that redirects once takes about two seconds. By that estimate, 200 such URLs on one host take roughly 7 minutes, well inside the default run timeout of one hour. The four-URL run above took 35 seconds, including start-up.
The limits, honestly
- Up to 5 redirect hops. An address with more hops gets a free
too-many-redirectsrow. Googlebot follows up to 10, so a 6 to 10 hop chain that Google would still follow shows here as an error. Either way, it needs fixing. - Old URLs that land on the same page. Only the first one gets a full row. The
others get a free
duplicate-pagerow whoseerror_detailnames the page they lead to, without their own hop list. Many such rows on one target are your signal for the "everything goes to the home page" problem; if you need every hop of every row, use the curl script. - robots.txt across domains. The robots.txt check reads the robots.txt of the site you listed. When old and new URLs are on different domains, open the new domain's robots.txt yourself; it is one file.
- Images and other files. An old URL that ends on an image, a PDF or another
file that is not an HTML page gets a free
not-htmlrow. It still shows the hops and the final status, but there is no page to check. - Quick mode reads the HTML as served. It catches a meta refresh, but not a
redirect or a
noindexadded by JavaScript after the page loads. - It is a polite bot. It identifies itself as QuarrywiseBot, honours robots.txt (an old URL that robots.txt disallows for it is not checked, and that row is free) and stops for a site that answers with a Cloudflare challenge, a CAPTCHA, 401 or 403. A site behind a bot wall gets free error rows instead of results. See our bot page.
- Duplicate titles are compared within one site. Pages are grouped by the site you listed (scheme and host), and titles are compared within that group.
- No keyword, ranking or backlink data, and it changes nothing on your site.
See the website health check on the Apify Store
FAQ
Should migration redirects be 301 or 308?
Either. Google treats both as permanent. A 308 also tells browsers to keep the request method, which matters for forms and APIs, not for normal pages.
How many redirect hops are too many?
Googlebot follows up to 10, but aim for one. Each extra hop slows the first visit, and chains tend to grow: the next migration adds another hop on top.
How long should I keep the redirects?
Google says at least a year, and as long as possible. Links from other sites keep pointing at the old URLs for years.
Can I redirect all old pages to the new home page?
Not without a cost. Google advises against redirecting many old URLs to one irrelevant page. Redirect each old URL to its closest match, and let truly gone pages answer 404 or 410.
Is a free redirect checker enough?
For status codes and hops, yes: the curl script above, a hosted redirect checker or a list-mode
crawler covers it. If you also want the target pages checked for noindex, canonicals
and titles in the same pass, the free version of a desktop crawler does that for up to 500 URLs.
Beyond 500 URLs you need a crawler licence or a hosted check such as ours.
Quarrywise is independent and isn't affiliated with or endorsed by Google, Screaming Frog, httpbin.org or Apify.
Sources
- Google Search Central: Site moves with URL changes
- Google Search Central: Redirects and Google Search
- Google Search Central: How HTTP status codes affect Google's crawlers (10 redirect hops)
- Screaming Frog: How to audit redirects in a site migration
- Screaming Frog SEO Spider: pricing (free 500 URL limit)
- Screaming Frog: list mode limited to 500 URLs without a licence
- curl manual (
--write-outvariables) - Apify Store search: redirect checker