The recommended default for scraping retries is truncated exponential backoff with full jitter, combined with strict respect for any Retry-After header the server sends. Only retry transient errors (429, 5xx), cap delays typically between 20 and 60 seconds, and pair the retry logic with observability and spending controls so a bad night doesn't turn into a banned IP or a runaway bill.
TL;DR:
- Retry only idempotent requests returning 429 or 5xx; surface 400, 401, 403, and 404 failures immediately because another attempt will not fix them.
- When a 429 or 503 includes a wait instruction, parse its seconds or date value, cap it safely, and skip jitter; otherwise use full jitter.
- Start with a 0.5 to 1 second delay, double each attempt, cap waits at 20 to 60 seconds, and stop after 3 to 8 attempts.
- For large crawls, track throttling and retry budgets per host, so one blocked domain does not slow unrelated sites or consume their retry allowance.
- Use retry quotas, request limits, and spending caps to bound runaway jobs; monitor 429 rates and IP blocks, then reduce concurrency or pause affected crawls.
Table of Contents
- Error Classification: Which Failures to Retry (and Which to Skip)
- Backoff Shapes and Jitter: Formulas, Variants, and Reference Code
- Respecting Server Signals: Retry-After Header and Rate-Limit Responses
- Tuning and Safety: Max Retries, Backoff Caps, Quotas, and Spending Controls
- Observability and Testing: What to Measure and How to Validate Behavior
- When to Use a Managed Web-Data API: How Gyrence Supports Safe Retries and Cost Controls
- Integrating Exponential Backoff with Scraping APIs Like Gyrence
- Best Practices for Combining Backoff with LLM Extraction Workflows
- Handling Rate Limits Across Large Site Graphs
- Backoff's Impact on Costs and Billing Predictability
- Real-World Recovery Scenarios Using Exponential Backoff
- Author Perspective: Three Anti-Patterns and Three Rules to Remember
- Practical Next Step: Try Gyrence for Budgeted, Observable Scraping
- FAQ
- Sources
Error Classification: Which Failures to Retry (and Which to Skip)
Retry logic only works when it knows the difference between a temporary hiccup and a permanent rejection. Retrying the wrong error class wastes requests, masks bugs, and accelerates bans.
Transient errors are worth retrying because the condition causing them often resolves on its own:
- 429 Too Many Requests: the server is rate limiting you right now, not rejecting you forever (MDN).
- 500, 502, 504: server-side failures, often transient load or upstream timeout issues.
- 503 Service Unavailable: overload or maintenance, frequently paired with a
Retry-Afterhint.
Permanent errors should not be retried, because retrying doesn't fix the underlying cause:
- 400 Bad Request: the request itself is malformed; retrying sends the same broken request again.
- 401 Unauthorized: credentials are missing or invalid.
- 403 Forbidden: usually a deliberate block, not a glitch.
- 404 Not Found: the resource isn't there, and won't appear because you asked twice.
A simple decision flow: if status is 429 or in the 5xx range, and the request is idempotent, retry with backoff. Otherwise, surface the failure immediately. This classification approach matches guidance from AWS on separating transient from permanent failures in retry design.
Backoff Shapes and Jitter: Formulas, Variants, and Reference Code
The baseline formula for truncated exponential backoff is:
delay = min(max_delay, base_delay * 2^attempt)
Doubling the delay on each attempt spreads out retries, but without randomness, every client hitting the same rate limit retries in lockstep, which recreates the exact spike that triggered the throttling in the first place. That's where jitter comes in.
Three common jitter strategies, as outlined in the AWS architecture blog on exponential backoff and jitter:
- Full jitter:
delay = random(0, min(max_delay, base * 2^attempt)). Maximizes randomness and minimizes synchronized retries. - Equal jitter: half the computed delay is fixed, half is random. A middle ground between predictability and spread.
- Decorrelated jitter: each delay is based on the previous delay rather than the attempt count, which smooths out retry timing across a fleet of clients.
AWS's own comparative analysis found full jitter reduces total client work and retry collisions more effectively than un-jittered or equal-jitter approaches, which is why it's the default we recommend for most scraping workloads.
A minimal sketch in pseudocode:
function getDelay(attempt, base=1, cap=30):
exp = min(cap, base * 2^attempt)
return random(0, exp)
Pro Tip: Seed your random number generator per worker process, not per request. Shared seeds across parallel workers can accidentally reintroduce synchronized retry timing even with full jitter.
Respecting Server Signals: Retry-After Header and Rate-Limit Responses
When a server sends a Retry-After header on a 429 or 503 response, that value is authoritative. It can arrive as a number of seconds or as an HTTP date, and MDN's documentation recommends honoring it rather than falling back to your own computed backoff.
Practical rules for combining server signals with client logic:
- Parse both formats: a bare integer (seconds) and an HTTP-date string.
- Clamp the server value within safe operational bounds, for example
min(server_delay, your_max_cap), so a misconfigured server can't stall your worker for hours. - Skip adding jitter to a server-specified delay. The server already picked a precise wait time; randomizing it defeats the purpose.
- When
Retry-Afteris missing or clearly wrong (negative, absurdly large), fall back to your own exponential backoff with full jitter.
Ignoring this header doesn't just waste a retry. It tends to escalate a temporary throttle into a longer or permanent block, since repeated requests against explicit server instructions read as abusive traffic rather than a bug.
Tuning and Safety: Max Retries, Backoff Caps, Quotas, and Spending Controls

Reasonable starting defaults for most scraping workloads: initial delay of 0.5 to 1 second, multiplier of 2, max delay capped between 20 and 60 seconds, and a max attempt count somewhere between 3 and 8, or a total time deadline instead of a raw attempt count. AWS's own SDKs commonly cap backoff in the 20 to 60 second range, which gives a tested starting point rather than a guess.
Beyond the formula, two mechanisms keep retries from becoming a liability:
- Retry quota (token bucket): each retry consumes a token from a limited budget; once the budget is exhausted, the system fails fast instead of looping, a pattern documented in AWS SDK retry behavior.
- Spending caps: a hard ceiling on API or infrastructure spend per job or per day, so a retry storm during an outage doesn't translate into a surprise invoice.
- Hard request limits: a ceiling on total requests per crawl, independent of retry count, to bound worst-case cost even if retry logic has a bug.
Defaults vary by use case. A low-value bulk crawl can tolerate aggressive caps and fewer attempts; a high-value single-page extraction can justify a longer deadline. The constant across both is a quota that stops retries before they become an outage of their own.
Observability and Testing: What to Measure and How to Validate Behavior
Retry logic without measurement is a guess. Track retry rate per endpoint, attempts per successful request, 429 and 5xx frequency over time, average backoff duration, unique IP block events, and cost per successful extraction.
A testing checklist that catches most retry bugs before production:
- Unit tests on the delay math itself: verify the cap, the jitter bounds, and the formula at edge attempts (0, 1, max).
- Integration tests that simulate 429 and 503 responses, confirming the client backs off and eventually succeeds or fails cleanly.
- Staged chaos tests that inject throttling into a non-production environment to watch how the system behaves under sustained rate limiting.
- Low-volume production smoke tests before scaling a new scraper to full throughput.
Set alerts on the signals that matter: a rising 429 ratio should trigger lower concurrency or a longer backoff cap, and a spike in unique IP blocks should pause the job entirely rather than retry through it.
When to Use a Managed Web-Data API: How Gyrence Supports Safe Retries and Cost Controls
Building and maintaining retry logic by hand is doable, but it's also where a lot of scraping projects quietly accumulate bugs: inconsistent error classification, missing jitter, no quota, no visibility into cost per successful call. A managed layer can absorb that complexity without hiding it.
- Our API returns typed, discriminated-union responses from every call, so different failure types are distinct, inspectable outcomes rather than one generic exception your retry code has to guess about.
- Spending caps can help bound the worst case: a retry storm during a target site's outage can't silently exceed your budget.
- Monitoring and webhook alerts can help drift or failure patterns surface as events instead of silent degradation.
- Implementation examples and sample retry code live in our scraping retries playbook and our guide to typed failure modes.
Integrating Exponential Backoff with Scraping APIs Like Gyrence
A retry loop built around a typed API behaves differently than one built around raw HTTP status codes, because the failure itself arrives as structured data instead of an exception to parse.
When a call through the API hits a transient condition, the response comes back as a discriminated failure type: rate limited, upstream timeout, or target unreachable, each distinguishable in code without string-matching an error message. That structure maps directly onto the retryable/non-retryable split covered earlier. A rate-limited response triggers backoff with jitter; a malformed-request failure does not, because retrying it would just repeat the same mistake.
The integration pattern involves a retry handler that switches on the failure type, applies full jitter backoff for transient cases, and respects any server-specified delay surfaced in the response metadata. Because the failure type is explicit, the retry handler doesn't need custom parsing logic per target site. The same handler can be used across multiple primitives returning the same response shape.
This matters most at scale. A scraper hitting a handful of sites can get away with ad hoc error handling. A pipeline running thousands of extractions a day needs the error classification to be mechanical, not something a developer has to reason about per target. Typed failures turn "is this worth retrying" from a judgment call into a lookup.
Best Practices for Combining Backoff with LLM Extraction Workflows
LLM-powered extraction adds a second failure surface on top of the network layer: the fetch can succeed while the extraction step times out, hits a model rate limit, or returns a response that doesn't match the requested schema.
Treat these as separate retry domains with separate policies. A network-level 429 from the target site and a model-side throttle aren't the same event, and collapsing them into one retry loop makes debugging harder. Fetch failures should retry against the fetch layer; extraction failures should retry against the extraction layer, with its own backoff cap, since LLM rate limits often recover faster than a blocked scraping target.
Schema validation failures deserve a different treatment entirely. Retrying an extraction that returned malformed JSON against the same prompt often reproduces the same malformed output. A better pattern is one retry with a stricter or clarified schema before falling back to a manual review flag, rather than looping the same request against an LLM rate limit.
Because LLM extraction is billed differently than raw fetches, in many pay-as-you-go setups it's the more expensive half of the call. That's a strong argument for fetching and caching the raw page once, then retrying extraction against the cached content rather than re-fetching and re-extracting together. Our guide to caching strategies covers the mechanics of reducing redundant fetches, which keeps extraction retries cheap by removing the network retry from the loop entirely.

Handling Rate Limits Across Large Site Graphs
Traversing a large site graph, hundreds or thousands of URLs under one domain, changes the math on backoff tuning, because a single slow-to-recover rate limit can stall an entire crawl if every worker backs off against the same ceiling.
The core adjustment is per-host rate tracking rather than global rate tracking. A crawler hitting five different domains shouldn't let a 429 from one domain increase backoff delays for requests to the other four. Isolating backoff state by host keeps one throttled target from degrading throughput across the whole job.
Concurrency also needs to flex with observed throttling. A fixed worker pool size that was fine for a lightly rate-limited site can trigger cascading 429s on a stricter one. A practical overview of rate-limiting algorithms covers several approaches, including adaptive concurrency, that pair well with exponential backoff when traversal volume is unpredictable.
For very large graphs, a retry quota per host, not just a global quota, prevents one problematic subdomain from exhausting the retry budget meant for the rest of the crawl. Combined with full jitter and a sensible cap, per-host isolation is the difference between a graph traversal that slows down gracefully on one path and one that stalls entirely because a single rate limit ate the whole retry budget.
Backoff's Impact on Costs and Billing Predictability
Retry logic has a direct line to your bill in any pay-as-you-go scraping setup, because every retried request is a request you pay for again. An un-tuned retry loop during a target site's partial outage can multiply request volume several times over before anyone notices.
Backoff caps and retry quotas aren't just reliability tools, they're cost controls. A retry quota that fails fast after a bounded number of attempts puts a ceiling on worst-case spend per job, which matters more in metered billing than in flat-rate billing, where the cost of a retry storm is invisible until the invoice arrives.
Spending caps at the workspace level add a second layer of protection on top of retry quotas: even if the retry logic itself has a bug, a hard cap on spend prevents that bug from becoming an unexpected charge. This is the practical argument for pairing retry tuning with a platform that bills predictably per call rather than per compute-hour, since predictable per-call billing makes it straightforward to calculate the worst-case cost of a given retry policy before running it.
Real-World Recovery Scenarios Using Exponential Backoff
A target site returning intermittent 503s during a deploy window is the clearest case for backoff working as intended: a scraper with full jitter and a 30-second cap waits out the deploy, succeeds on retry two or three, and never needs an alert.
A 429 flood from a single aggressive crawl pattern looks different. Here, backoff alone buys time but doesn't fix the root cause, which is usually concurrency set too high for that host. The fix is lowering per-host concurrency in response to the 429 ratio, not just increasing the backoff cap, since a longer wait on the same request rate just delays the next flood.
A site that sends Retry-After: 120 on every 429 during a known maintenance window is a case for respecting the server signal exactly rather than second-guessing it with a shorter computed delay. Retrying earlier than instructed, even "to be safe," tends to extend the block rather than shorten it.
The hardest case is a silent failure: no error code, just an empty or malformed response. This isn't a backoff problem at all, since there's no transient signal to retry against; it's a schema validation and alerting problem, which is why observability metrics matter as much as the retry formula itself.
Author Perspective: Three Anti-Patterns and Three Rules to Remember
The three mistakes I see most often: no jitter (synchronized retries recreate the spike), ignoring Retry-After (turns a throttle into a block), and unbounded retries (an outage becomes an infinite loop).
Three rules worth keeping taped above your desk: classify before you retry, cap every delay, and measure retry rate like it's a production metric, because it is. For hands-on examples of these mistakes in the wild, our roundup of common scraping mistakes is worth a read.
— Glen
Practical Next Step: Try Gyrence for Budgeted, Observable Scraping
If you're implementing the backoff logic above by hand, the next question is usually how to keep it from becoming its own maintenance burden. That's the job primitives are built for: typed, discriminated failure responses so your retry handler never has to guess at error types, spending caps so a retry storm can't turn into a surprise invoice, and bundled LLM-guided extraction to simplify billing for structured output.
To get started:
- Check pricing and plans, including the Free and Founders tiers for early testing.
- Read the docs for request and response shapes across Search, Traverse, Fetch, Extract, and Map.
- Set up WebDoppler monitoring before running anything at volume.
- Run a low-volume test job first to validate your retry handling against real typed failures.
FAQ
What is the purpose of exponential backoff?
Exponential backoff exists to space out retry attempts so a client doesn't hammer a struggling server with requests at a constant rate. Increasing the delay after each failed attempt, commonly by doubling it, gives the server time to recover from load or a rate-limit window, reducing the chance of a temporary block becoming a permanent one.
What is the exponential backoff strategy?
The strategy computes each retry delay as a multiple of a base delay raised to the power of the attempt count, capped at a maximum value, written as delay = min(max_delay, base * 2^attempt). Production implementations add randomness, called jitter, on top of that formula to prevent multiple clients from retrying in sync, a pattern detailed in AWS's architecture blog.
Why add jitter to exponential backoff?
Jitter adds randomness to each computed delay so that many clients retrying after the same failure don't all retry at the exact same moment. Without it, synchronized retries recreate the same traffic spike that caused the original rate limit, and AWS's comparative analysis found full jitter reduces both collision rates and total retried work compared to un-jittered backoff.
What is exponential backoff in TCP and how does it work?
TCP uses exponential backoff to manage retransmission timing when packets are lost or acknowledgments don't arrive in time, doubling the retransmission timeout after each failed attempt up to a cap. The same underlying principle, spacing out retries exponentially to avoid overwhelming a congested link, is what application-level scraping retries borrow for handling 429 and 5xx responses.
Should I always respect the Retry-After header?
Yes, when a server sends Retry-After on a 429 or 503 response, MDN's documentation recommends honoring that value rather than substituting your own computed delay. Clamp it within a safe operational maximum to avoid an unusually large or malformed value stalling a worker indefinitely.
Sources
- Exponential Backoff And Jitter | AWS Architecture Blog
- Timeouts, retries, and backoff with jitter
- 429 Too Many Requests - HTTP | MDN
- Retry behavior - AWS SDKs and Tools

