URL lists and batch jobs
Retry policy for batch fetches: what to retry, what to skip
Which failed page fetches to retry, how long to wait between attempts and which errors stop a batch job, with a small Python function that applies the rules.
By Caitlin Sanders, AgentScraper team · Published
This article was drafted with AI assistance from the sources listed below and edited and published by the AgentScraper team. Disclosure: the AgentScraper API, which this article mentions, is this site's own product.
- 429: wait Retry-After, then retry, up to 3 times
- 5xx or network error: retry up to 3 times with backoff
- CRAWL_LIVECRAWL_TIMEOUT: retry up to 3 times with backoff
- Any other per-URL error: no retry in the same run
- RESTRICTED_TARGET or 451: never retry
- 402, 401 or 403: stop the whole job
A batch job that retries everything is slow and impolite. A job that retries nothing loses pages to every slow server. The useful middle is a short policy by error type, applied the same way in every script, workflow and scheduler. This post writes that policy down once, as a table and as a function you can reuse.
The policy
This is the policy of the batch scripts in the Automate requests docs, and the one every guide on this site uses: retry 429, 5xx and CRAWL_LIVECRAWL_TIMEOUT up to 3 times with backoff, retry no other per-URL error in the same run, and stop the job on 402, 401 or 403.
The full table, with every error tag and the reason behind each row, is in the retry rules for URL lists. The short version has three groups:
- Retry now: the problem is about timing. You went too fast (
429), the server had a moment (5xx, a dropped connection), or the target site was slow (CRAWL_LIVECRAWL_TIMEOUT). - Do not retry in this run: the answer will not change in seconds.
CRAWL_NOT_FOUND,CRAWL_NOT_RENDEREDandUNSUPPORTED_CONTENT_TYPEare final for this URL.CRAWL_UNKNOWN_ERRORandSOURCE_NOT_AVAILABLEare left for the next run.RESTRICTED_TARGETand451are never retried at all. - Stop the job: the problem is the account, not the page.
402means the credits ran out;401and403mean the key is invalid, revoked or suspended. Every further request would fail the same way.
How long to wait
For 429, the server tells you: the Retry-After header holds the number of seconds to wait. RFC 6585, which defines status 429, says the response may include it, and this API sends it.
For 5xx and timeouts, use exponential backoff with jitter. The batch scripts wait 0.5 s × 2^attempt plus a random 0 to 250 ms:
| Retry | Wait |
|---|---|
| 1st | 0.50 to 0.75 s |
| 2nd | 1.00 to 1.25 s |
| 3rd | 2.00 to 2.25 s |
The worst case is under 4.3 seconds of waiting per URL before it is marked as failed. The jitter matters when several workers run in parallel: without it, workers that failed together retry together and fail together again. The AWS Architecture Blog post in the sources shows this effect with simulations.
One function for every script
Put the decision in one place, so a cron script, a worker pool and a notebook all behave the same:
import random
RETRY_NOW = {"CRAWL_LIVECRAWL_TIMEOUT"}
STOP_JOB = {401, 402, 403}
MAX_RETRIES = 3
def next_step(http_status: int, tag: str | None, retries_done: int, retry_after: str | None = None):
"""Return ("retry", seconds), ("skip", None) or ("stop", None)."""
if http_status in STOP_JOB:
return "stop", None
if retries_done >= MAX_RETRIES:
return "skip", None
if http_status == 429:
return "retry", float(retry_after) if retry_after else 0.5 * 2 ** retries_done + random.uniform(0, 0.25)
if http_status >= 500 or (http_status == 200 and tag in RETRY_NOW):
return "retry", 0.5 * 2 ** retries_done + random.uniform(0, 0.25)
return "skip", None # 400, 451 and every other per-URL error tag
Call it after each attempt with the HTTP status, the per-URL error tag (from statuses[0].error.tag when the status is 200), the number of retries so far and the Retry-After header. Treat a dropped connection like a 5xx. Record the final outcome in your output line: success, the error tag, or skipped when the job stopped.
Make the next run do the rest
The policy only works if the output remembers what happened. Write one line per URL with its final status and error tag. At the start of the next run, skip URLs that succeeded or failed for good (CRAWL_NOT_FOUND, RESTRICTED_TARGET), and send the rest again: CRAWL_UNKNOWN_ERROR, SOURCE_NOT_AVAILABLE, timeouts that ran out of retries, and URLs skipped after a stop.
If the same host returns SOURCE_NOT_AVAILABLE run after run, stop collecting it. Repeated refusals are a message from the site; the responsible web scraping guide covers what to check.
What this means for your workflow
- Retry
429,5xxand timeouts up to 3 times; retry nothing else in the same run. - Wait
Retry-Afteron429; use backoff with jitter for the rest. - Never retry restricted targets; stop the job on
402,401or403. - Keep the decision in one function and one line of output per URL.
- Let the next scheduled run handle what the current one could not.
Frequently asked questions
Why not retry every failed page a few times?
Because most failures do not go away in a few seconds. A missing page stays missing, a page that needs JavaScript still needs it, and a restricted target stays restricted. Retrying those wastes time, and retrying restricted targets can get a key suspended.
Why wait for the next run instead of retrying now?
A site that refused access or failed for an unknown reason is unlikely to change its answer within seconds. Hours later it might. Marking the URL and letting the next scheduled run pick it up is gentler on the site and keeps the current run short.
Sources
- Retry-After header (MDN) (Documentation)
- RFC 6585, section 4: 429 Too Many Requests (Documentation)
- Exponential backoff and jitter (AWS Architecture Blog) (Blog)