← All posts

API Rate Limit Retry Strategy for Headless Browser APIs

September 24, 2026

Screenshots and PDFs aren't like typical API calls. A render can take seconds, spin up a real browser process, and burn a billed credit — so retry logic that's fine for a lightweight JSON endpoint can quietly wreck both your uptime and your invoice when applied to a rendering API. If you've scaled a headless browser integration from a handful of test requests to hundreds of concurrent jobs and started seeing 429s, timeouts, and inflated usage, the fix isn't "retry harder." It's a deliberate API rate limit retry strategy built around how rendering workloads actually fail.

Why Naive Retries Make Headless Browser Rate Limits Worse

The most common pattern in early-stage integrations is a for loop that retries immediately on any failure, maybe with a fixed one-second pause. At low volume this looks harmless. At scale, it's the mechanism that turns a brief capacity blip into a full outage.

Here's the failure mode specific to headless browser API rate limits: each render already consumes meaningful CPU, memory, and browser-process time server-side. When you're running batches or loops — say, 500 PDFs generated in a CI job, or a scraping pipeline firing concurrent screenshot requests — a rate limit response usually means the backend is already near saturation. If every failed request retries after the same fixed delay, all those retries land at once, adding a second wave of load on top of a system that just told you it was overwhelmed. This is the classic thundering-herd problem: synchronized retries cluster together instead of spreading out, and each retry storm makes the congestion worse. AWS's engineering team documented this exact dynamic in their research on exponential backoff and jitter, and it applies directly to any client hammering a rate-limited render endpoint.

The practical cost: more 429s, more timeouts, and — because each retried render might still be billed — a growing pile of wasted credits for work that never made it back to your pipeline.

Classify the Error Before You Retry

Not every failure deserves a retry. Blindly retrying everything wastes render credits on requests that were never going to succeed, and it hides real bugs behind a retry loop that just keeps masking them.

Retryable responses signal a transient condition: 429 Too Many Requests, 503 Service Unavailable, connection timeouts, and general 5xx server errors. These indicate the server (or network) is temporarily unable to handle the request, not that the request itself is invalid.

Non-retryable responses indicate a client-side problem that retrying won't fix: 400 Bad Request (malformed payload), 401/403 (auth or permission issues), and 422 (invalid parameters — a bad selector, an unsupported viewport size, a malformed URL). Retrying these just burns credits and delays the moment you notice your code has a bug. A sound retryable vs non-retryable classification step is the single highest-leverage piece of resilient API client design, because it stops wasted spend before backoff logic even matters. It also matters for handling 429s specifically: a 429 means "slow down," not "something is broken," so it deserves patience rather than alarm.

Respect the Retry-After Header First

Before writing any custom backoff math, check for a Retry-After header. Per RFC 6585, which defined the 429 status code, and the updated guidance in RFC 9110, servers can tell clients exactly how long to wait — and honoring that value is simpler and more effective than guessing.

Retry-After comes in two valid forms: delta-seconds (a plain integer like Retry-After: 20, meaning wait 20 seconds) and an HTTP-date (Retry-After: Wed, 21 Oct 2025 07:28:00 GMT, meaning wait until that timestamp). Your client should parse both: try an integer first, then fall back to parsing an HTTP date and computing the delta from now. MDN's reference on 429 and Retry-After is a good implementation checklist. If the header is present, use it as the wait time — don't override it with a shorter backoff, since the server is giving you a direct signal about its own recovery window.

Capped Exponential Backoff With Full Jitter

When no Retry-After header is present, fall back to capped exponential backoff with jitter — the pattern AWS's SDKs use internally and the one most resilient clients converge on. Each retry waits longer than the last (exponential growth), the wait is capped so it doesn't grow unbounded, and randomness (jitter) is added so concurrent clients don't retry in lockstep.

base = 500ms
cap = 30s
attempt = 0

function nextDelay(attempt):
    exp = min(cap, base * 2^attempt)
    return random_between(0, exp)   # full jitter

on failure (retryable):
    if attempt >= max_attempts: raise
    delay = nextDelay(attempt)
    sleep(delay)
    attempt += 1
    retry()

"Full jitter" (random between 0 and the capped exponential value, rather than a fixed exponential plus a small random offset) performed best in AWS's own testing and is a reasonable default for a rate limit backoff algorithm. Cap the exponent (commonly around 30–60 seconds) so a degraded dependency doesn't leave requests waiting minutes for no benefit, and set a hard max_attempts so failures eventually surface instead of retrying silently forever.

Add a Circuit Breaker for Sustained Failures

Backoff handles transient blips. It doesn't handle a sustained outage or a plan-level concurrency ceiling you've exceeded — continuing to retry into that situation just keeps hammering an API that isn't going to recover on its own timeline. This is where a circuit breaker pattern earns its place in resilient API client design.

The mechanics are simple: track consecutive failures per endpoint or job type. After a threshold (commonly 5–10 consecutive failures), open the circuit — stop sending requests entirely and fail fast for a cooldown window (30–60 seconds is a reasonable start). After the cooldown, allow a single "trial" request through; if it succeeds, close the circuit and resume normal traffic, if it fails, keep the circuit open and extend the cooldown. Failing fast during an open circuit protects the rest of your batch or CI pipeline from stalling on a dependency that's already told you it's unavailable.

Guard Against Duplicate Billed Renders

Retries introduce a cost problem unique to rendering APIs: if a request times out after the server already generated the screenshot or PDF, a naive retry produces a second billed render for work that already completed. Over a large batch job, this silently inflates render credit usage with no corresponding benefit.

Two safeguards fix this. First, use an idempotency key: generate a unique key per logical request (a hash of the URL, viewport, and options) and send it with retries of the same request, so the server can recognize and return the original result instead of re-executing it. Second, before retrying a timeout specifically, consider a lightweight status check if your job has an ID or webhook callback, so you're not re-firing an operation that already finished server-side. Combining idempotent retries with a caching strategy for screenshot workloads further cuts the number of renders competing for your rate-limit budget in the first place.

Putting It Together: A Minimal Resilient Client Checklist

A production-ready client for a headless browser API should, in order: classify the error (retryable vs not), check for and honor Retry-After, fall back to capped exponential backoff with full jitter, track failures and open a circuit breaker after a sustained streak, attach idempotency keys to avoid duplicate billed renders, and log/monitor retry counts so rate-limit pressure shows up in your dashboards before it becomes an incident. That's the whole API rate limit retry strategy — five deliberate steps replacing one naive loop.

Check Browsevra's docs for the actual rate-limit headers, error codes, and concurrency limits your client should implement against — the pattern above is only as good as the specifics it's tuned to. And if you're retry-storming because you've simply outgrown your current concurrency cap, it's worth checking Browsevra's pricing before adding more retry logic on top of a limit that a plan change would solve. Learn more at browsevra.

Frequently Asked Questions

What's the difference between a 429 and a 503 error, and should I retry both the same way?

A 429 means the server understood your request but you've exceeded a rate limit, while a 503 means the service is temporarily unavailable, often due to overload or maintenance. Both are retryable, but a 429 should almost always honor a Retry-After header if present, whereas a 503 more often falls back to your exponential backoff logic since it may not include timing guidance.

How many times should my client retry before giving up?

There's no universal number, but 3–5 attempts with capped exponential backoff is a common, safe default before surfacing a failure. Beyond that, repeated failures usually indicate a sustained issue — like a concurrency ceiling or outage — better handled by a circuit breaker than by more retries.

Should I retry a request that timed out but might have already completed on the server?

Not blindly — check the job status or use an idempotency key first if your API supports it, since the render may have already succeeded and been billed. Retrying without that check risks generating and paying for a duplicate screenshot or PDF.

Do I need a job queue to handle retries, or can I do it in-process?

In-process retry logic with backoff and a circuit breaker is enough for moderate volume, but a job queue becomes worthwhile once you're running large batches, need retry state to survive process restarts, or want centralized concurrency control across workers. The queue doesn't replace the retry pattern — it just gives it a durable place to live.

How do I know what Browsevra's actual rate limits and concurrency caps are?

The exact rate-limit headers, error codes, and concurrency limits for your plan are documented in Browsevra's docs. Checking there first ensures your backoff caps, retry counts, and circuit breaker thresholds are tuned to real numbers rather than guesses.