output_format, Fetch with allowed_status_codes, Batch
How it works
- Crawl reads the site. A crawl with
"output_format": "html"keeps each matching page’s HTML. The download endpoint returns every page in one NDJSON file. - Your script collects the links. It parses each page’s
<a href>links, keeps the ones on the site’s own domain, and records which pages link to each one. - Fetch checks each link. With
allowed_status_codes=404,410, Fetch answers200and puts the target’s status in theZr-Status-Codeheader. A link that loads normally has noZr-Status-Codeheader and answers200. - You get the report. Every link with a status of 400 or more is listed with the pages to fix.
Ask your AI agent
With the Zenrows MCP server connected to Claude, Cursor or any MCP client, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.Check every link on a site
Save this asbroken_links.py and run it. It needs only requests.
Python (Requests)
stream=True reads the response headers and leaves the page body undownloaded, which keeps each check fast. Each check is one Fetch request, and mode=auto loads links on protected sites too. Fetch bills a 404 or 410 answer like any other page.
Check one link
To check a single URL, send it withallowed_status_codes and read the Zr-Status-Code header:
cURL
Output
allowed_status_codes, Fetch answers a missing page with its own 404 and the error code RESP002, which also tells you the link is broken.
Check thousands of links with Batch
For a large site, send the links to one Batch job instead of a thread pool. Leave outallowed_status_codes: each link that answers 404 then becomes a failed row whose error holds "status": 404 and "code": "RESP002".
Python (Requests)
Output
next_cursor as shown in Scrape a list of URLs. The run’s stats.failure_reasons counts failures by kind, for example {"bad_target": 1}.
Example output
Every link on scrapingcourse.com loads, so this run adds one made-up URL,/product/does-not-exist/, to the links found on the Abominable Hoodie page before the checks. Everything else is the real run:
Customize it
Troubleshooting
- A link reports
422or429instead of a page status: that code is Fetch’s answer, not the target’s. Check the link again. For429, setmax_workersto the value of theConcurrency-Limitresponse header. - The script finds few links: the crawl kept few pages. Raise
max_itemsandmax_pages, and add the paths of listing pages toinclude_patterns. - Links show up twice with and without a trailing slash: the site links to both spellings. Normalize them before checking, for example with
link.rstrip("/"). 429withtoo_many_crawls: other Crawl and Batch jobs on your account are still running. Wait the number of seconds in theRetry-Afterheader, then start the crawl again.
Related recipes
Map every URL on a website
List a site’s URLs without their HTML.
Get alerted when a website adds new pages
Compare a section’s URLs run after run.
Scrape a list of URLs
Send thousands of URLs as one Batch job.
Crawl a website from a start URL
Get every matching page with its HTML.