I cap incoming work before tuning retries. Three retry layers allowing three attempts each can turn one request into 27 provider calls. Longer waits alone won’t stop the queue growing.
My approach is to:
- Bound queues and active requests, rejecting or deferring excess work within set limits.
- Honour
Retry-After, then add capped exponential backoff with jitter. - Give each request one retry owner, one attempt budget and one deadline across all layers.
- Retry only eligible failures, and prevent duplicate side effects when a timeout leaves the outcome unclear.
- Cap retry spending across the service, and check capacity again before each retry.
- Track queue age, attempts per request and completed work - not just falling error rates.
The rule I follow: limit the work, not just the retry pace. Then test overload and restore capacity gradually so recovery doesn’t trigger another wave.
::: @figure
{Stop LLM Retry Storms with Backpressure}
:::
Control Retry Timing with Retry-After, Backoff and Jitter
Once a failure is retryable, space out retries without letting queues grow.
Backoff spreads retries; backpressure limits admitted work. Use timing controls alongside bounded queues, concurrency limits and retry budgets. [1][2]
| Strategy | Timing behaviour | Overload risk | Appropriate use |
|---|---|---|---|
| Fixed delay | Retries after the same interval | Synchronised retry waves | Small, controlled workloads or deliberately limited retry attempts |
| Exponential backoff with jitter | Expands the retry window and randomises each wait | Fewer bursts, but queued work can still grow without limits | General transient failures, subject to deadlines, retry budgets and queue limits |
| Retry-After-guided backoff | Waits for the provider’s stated minimum, then adds permitted client-side spreading | Lowest when clients honour the minimum wait; risk remains if they ignore it or queue too many requests | Rate limits, throttling and overload responses |
Use Retry-After as the Minimum Wait
Accept both delta-seconds and HTTP-date values. Start timing when the response arrives. For HTTP-date, calculate the delay from that receipt timestamp.
Use a synchronised system clock to interpret dates and a monotonic clock to measure elapsed waits. Account for clock skew and clamp negative results to zero. If the remaining deadline is shorter than the required wait, stop - don’t shorten the wait. Fall back to configured backoff when guidance is missing, malformed or unreliable. [4][7]
Use Capped Exponential Backoff with Jitter
For retry n, use B=min(Bcap, B0×2^n), starting at n=0. The goal isn’t just to slow retries down. It’s also to keep fewer retries queued at once.
A 1-second base with an 8-second cap yields 1, 2, 4 and 8-second windows, then holds at 8 seconds.
Full jitter samples uniformly within that window. The cap limits delay growth, not the number of attempts. [3][5]
| Jitter method | Calculation | Timing effect |
|---|---|---|
| Full jitter | J=U(0,B) |
Spreads retries across the whole window; some occur early |
| Equal jitter | J=B/2+U(0,B/2) |
Avoids very short waits but narrows the spread |
| Additive jitter | J=B+U(0,A) |
Keeps the base delay; the additive range must be bounded |
Here, U means uniform random sampling, and A is a separately configured maximum offset. Use full jitter as the default for large, independently retrying clients. Choose equal or additive jitter when you need a minimum client-side delay.
Add Jitter After the Provider's Minimum Wait
Calculate the capped window B, then sample J=U(0,B). Without valid provider guidance, wait J. With a remaining provider wait R, wait W=R+J. Cap the client-side jitter window - not the provider’s minimum.
Schedule the retry only when W + execution allowance ≤ remaining deadline, the retry budget allows another attempt, and queue and concurrency limits allow admission. Otherwise, stop or defer.
Limit Queue Size and Concurrent Requests
Even well-spaced retries need admission control. Backpressure caps both waiting and active work. Backoff slows retries, but bounded queues and concurrency limits stop the backlog from growing. Apply those limits to deferred jobs, timers and retry counts too.
Reject or Defer Work When Queues Are Full
Set queue capacity using acceptable waiting time, throughput the system can maintain and the memory budget - not peak demand. Cap both item count and payload size, and expire work that cannot finish before its deadline. Spare queue capacity does not make a queue safe if its oldest request is already past its deadline.
| Queue design | Failure mode | User impact |
|---|---|---|
| Unbounded queue | Unchecked backlog and resource growth | Timeouts or stale results |
| Bounded queue with rejection | Excess work is rejected | Fast failure |
| Bounded queue with deferred processing | Retention limits are reached | Later completion or explicit expiry |
Reject excess interactive requests with HTTP 429 for rate limiting or 503 for temporary service unavailability. Include Retry-After with either response when the client can retry later.
Defer background work only if the system supports asynchronous processing. Use bounded durable storage, deduplication, expiry and a dead-letter policy.
Once retry timing is bounded, control queue depth and active concurrency.
Adjust Admission Using Queue and Provider Signals
Monitor queue depth, oldest-queued age, active requests, provider 429 rates and retry volume together. Tighten admission when these signals worsen, and reduce concurrency when provider capacity is constrained. Even a short queue needs attention if its oldest item has waited too long.
Restore capacity gradually after a stable recovery window. Don’t release the whole backlog at once.
Budget tokens across replicas, not just per worker. Reserve estimated input and output tokens, account for request duration, and use shared limits or conservative per-replica allocations.
Release slots while waiting to retry. Treat each delayed retry as new work at admission: re-check its deadline, retry budget, queue depth and token availability. A timer firing must not bypass admission control.
Set Retry Budgets and Check Recovery
Backpressure limits queued work. Retry budgets limit the extra work retries create.
Set one retry policy per logical request, covering maximum attempts, an absolute deadline, maximum backoff and eligible error classes. Add a capped, service-wide retry token bucket: each retry spends a token, successful requests refill it gradually, and an empty bucket blocks further retries. Size the bucket against provider quotas and spare capacity. This caps extra work, but does not replace admission control.[8][10]
Enforce Deadlines Across Retry Layers
Pass the same deadline and attempt counter through the SDK, gateway, worker and fallback path. The deadline must cover queueing, time in deferred storage, Retry-After waits, backoff, network time and execution.[6][9]
Measure Retry Amplification and Queue Recovery
Once retry spending is capped, check whether the system is recovering.
Track retry amplification, retry-token depletion, completion rate and cost per completed logical request (£). Report rejected, expired and dead-lettered work separately. Otherwise, reduced load can look like recovery.
Count logical requests, initial attempts and retries separately. Retry amplification = total provider attempts ÷ logical requests. Fewer 429 responses alone do not prove recovery.[6]
Test Overload Controls and Provider Recovery
Test the same limits you enforce in production. Inject 429s, 503s, slow calls, invalid Retry-After headers and waits that exceed request deadlines. Test simultaneous failures across replicas, plus sustained arrivals above provider capacity.
Set pass criteria before testing: queue, deferred-storage, concurrency and retry limits must hold. Excess work must be rejected or expire within a known time. Verify that the backlog drains without a second retry wave.[6]
Conclusion: Limit Work Before Retrying
Cap admitted work first; tune retry timing second.
With admission capped, use Retry-After as the minimum wait. Then apply capped exponential backoff with jitter, stopping if the wait exceeds the remaining deadline.[11][1][3]
Once timing is under control, cap total retry spend with a single end-to-end attempt budget and deadline. Restore admission gradually, and only when queue age, retry amplification and provider errors have stabilised. These limits keep queues from growing without bound.
FAQs
How do I size queue limits and retry budgets?
Set queue limits by acceptable waiting time, not peak demand. Use Little’s Law (average in-flight work = throughput × response time). Pair task counts with token or memory budgets to account for differences in payload size.
Cap retries at 5% to 10% of total traffic to limit load amplification. Apply the same admission controls and deadlines to retries as you do to new work. Discard requests that can no longer meet their original deadline.
Should I retry an LLM request after a timeout?
Only retry an LLM request when the timeout is temporary and the operation is idempotent - safe to repeat without duplicate side effects. Keep retries in one layer, set a strict attempt limit and stick to the original absolute deadline.
Treat Retry-After as the minimum wait. Otherwise, use capped exponential backoff with jitter. If there isn’t enough time left for a retry to succeed, defer or reject the work.
How can I prevent retries from starving new requests?
Apply the same backpressure controls to retries and new requests. Put both in a shared, bounded priority queue so they compete for the same resources. Retries must not bypass admission checks.
When the system is overloaded, drop low-priority work and reject excess requests. Don’t let retries pile up indefinitely.
Set a strict retry budget, typically 10% of total traffic. Carry the original deadline through every attempt and drop tasks once that deadline has passed.