When a server returns HTTP 429, stop sending immediate retries. Honor a valid Retry-After value when present; otherwise use capped exponential backoff with jitter. Before every retry, check cancellation, the operation’s safety and a shared retry budget. Once the budget is exhausted, return a clear failure instead of extending the incident with more traffic.
What 429 tells the client
HTTP 429 Too Many Requests means the client has sent too many requests within some period. A response may include Retry-After, but the status alone does not reveal the server’s quota window, whether the limit applies per user or per IP, or whether another attempt will succeed soon.
Treat 429 as deliberate load feedback, not an ordinary transient network error. Tight retry loops amplify pressure on an already constrained service. In a fleet of workers, simultaneous retries can create a second traffic spike just as capacity becomes available.
The first response should also depend on the caller. An interactive request may have only a short latency allowance. A background synchronization job can often pause longer. A queue consumer may reduce concurrency instead of repeatedly retrying individual messages.
Parse Retry-After defensively
Retry-After has two standard forms: a non-negative delay in seconds, such as Retry-After: 12, or an HTTP date. Delay seconds are straightforward. For a date, calculate the difference from the current time, preferably accounting for the response’s Date header when available because client and server clocks may disagree.
Parsing must not turn server input into an unbounded sleep. Reject invalid and negative values, and stop or defer the operation if the requested delay exceeds its deadline. Do not shorten a valid server-requested wait and then retry early. If the header is missing or unusable, fall back to the client’s backoff policy. A past HTTP date should generally become a zero base delay, but jitter and budget checks should still prevent an immediate synchronized retry.
function retryDelay(response, attempt, now, remainingBudget) {
const advised = parseRetryAfter(response.headers, now)
const fallback = min(baseDelay * 2 ** attempt, delayCap)
const delay = advised.valid
? advised.ms + randomBetween(0, jitterCap)
: randomBetween(0, fallback)
if (delay > delayCap || delay >= remainingBudget) return null
return delay
}This sketch expresses policy rather than prescribing one library. A null result means stop or defer the operation, never retry immediately. The caller must also reserve enough time for the request itself and honor cancellation. In production, use a monotonic clock to track elapsed budget and wall-clock time only when interpreting an HTTP date.
Combine backoff with jitter
Exponential backoff spaces repeated attempts: for example, a policy can grow from one base interval to two, four and eight intervals, then stop growing at a cap. The exact base and cap belong to the service contract and user-facing deadline; copying arbitrary constants between a payment API and a batch indexer is poor engineering.
Jitter randomizes the wait so clients that failed together do not retry together. Full jitter selects a random value between zero and the calculated cap. Equal jitter preserves part of the base wait while randomizing the remainder. Either can work if consistently implemented; no jitter is the dangerous default in distributed systems.
When Retry-After exists, do not retry before the indicated wait has elapsed. A conservative client can avoid retrying before that delay and add a small randomized spread afterward. Document this choice, particularly when an upstream provider defines precise semantics.
A retry budget needs more than max attempts
A fixed attempt count is useful but incomplete. Three retries that each wait thirty seconds are different from three retries completed in one second. Define a budget across several dimensions:
| Budget dimension | Question before retrying | Typical action |
|---|---|---|
| Attempts | How many extra calls has this operation made? | Stop at the configured ceiling. |
| Elapsed time | Can another wait and request finish before the deadline? | Abort when insufficient time remains. |
| Request volume | Are retries consuming too much of the client’s traffic? | Use a shared token or percentage budget. |
| Operation safety | Could replay duplicate a side effect? | Require idempotency protection or do not retry. |
| Cancellation | Does anyone still need the result? | Cancel sleep and in-flight work promptly. |
A shared budget matters when many requests fail at once. Without it, every operation can stay within its personal limit while the application collectively overwhelms the dependency. Reserve retry capacity with a token bucket, concurrency limit or small retry allowance tied to successful first attempts.
Retry only operations you can replay safely
Reading a resource is usually easier to replay than creating an order or charging a card. Method names alone are not sufficient: a nominally safe endpoint can trigger unusual side effects, while a write endpoint can support an idempotency key.
For state-changing requests, retry only when the API provides a reliable deduplication mechanism and the client reuses the same idempotency key and payload. A fresh key converts a retry into a new operation. Also distinguish a definite 429 response from a connection loss where the client does not know whether the server committed the request.
Implementation checklist
- Classify the operation: record whether replay is safe, conditionally safe with an idempotency key, or forbidden.
- Parse both header forms: support delay seconds and HTTP dates; reject malformed or negative input.
- Set hard caps: limit attempts, individual delay, total elapsed time and retry traffic.
- Add jitter: randomize schedules across processes and instances.
- Propagate cancellation: make both waiting and network calls interruptible.
- Reduce demand: lower concurrency, pause queue intake or batch work where appropriate.
- Expose the outcome: log attempt count, chosen delay, header validity, endpoint class and final reason without leaking secrets.
- Test deterministically: inject the clock and random source; cover missing headers, dates, malformed values, caps, cancellation and exhausted budgets.
The essential rule is simple: a retry must consume a finite budget and improve the chance of success. If it merely repeats the same demand at nearly the same moment, it is not resilience—it is additional load.
For the replay decision, see idempotency in HTTP APIs.