Retries are one of the simplest reliability tools in distributed systems. A transient network failure, a short leader election, or a momentary overload can make a request fail even though the dependency becomes healthy again a few milliseconds later. Retrying can hide that temporary failure from the user.
The same mechanism can also make an outage worse.
If a struggling dependency starts failing requests and every caller retries immediately, the dependency receives extra work precisely when it has the least capacity to handle it. A small failure rate can turn into a retry storm: retries create more load, more load creates more failures, and those failures create still more retries.
Backoff and jitter help control when retries happen. They do not answer a different question:
How many additional requests are callers allowed to create?
A retry budget answers that question. It limits retry traffic relative to normal traffic so retries remain a bounded recovery mechanism instead of becoming an unbounded source of amplification.
Why retries can amplify a partial failure
Suppose a service normally sends 10,000 requests per second to a dependency.
If 20% of those requests fail and every failed request is retried once, the callers create up to 2,000 additional requests per second.
original traffic: 10,000 req/s
failed requests: 2,000 req/s
one retry each: +2,000 req/s
-----------------------------
possible total: 12,000 req/sThat is a 20% traffic increase caused by a dependency that is already failing.
Now imagine three retry attempts instead of one. If failures persist, the extra traffic can be much larger. The exact number depends on which attempts fail and which requests are still within their deadlines, but the direction is clear: retry work is not free.
The important mental model is:
useful load
+
recovery load
=
total load seen by the dependencyRetries are recovery load. A reliable design controls that load deliberately.
Backoff controls timing, not total retry volume
Exponential backoff is useful because it spreads repeated attempts over increasing delays. Jitter is useful because it prevents many clients from waking up at exactly the same instant.
Those mechanisms solve synchronization and pacing problems.
They do not impose a hard upper bound on the amount of retry traffic.
For example, a client can correctly use exponential backoff and still allow every failed request to make three more attempts. During a sustained outage, that policy can keep generating significant retry traffic for every new request that enters the system.
A retry budget adds another control:
Should this failed request be retried at all?The decision becomes:
failure
|
+--> retryable error?
|
+--> enough time left?
|
+--> retry budget available?
|
+--> backoff + jitter
|
+--> next attemptEach check removes a different class of harmful retry.
Define a retry budget relative to ordinary traffic
A practical retry budget is often expressed as a fraction of original request volume.
For example, a service might allow retry traffic equal to at most 10% of original traffic over a rolling window.
If the service sends 50,000 original requests during that window, the budget permits at most 5,000 retries.
original requests: 50,000
retry budget: 10%
allowed retries: 5,000This has an important property: retry capacity scales with actual workload instead of being an unrelated fixed number.
The exact accounting model can vary. Some systems replenish retry tokens from successful requests. Others compute a rolling ratio between retries and original attempts. The implementation is less important than the invariant:
retry traffic must have an explicit upper bound that does not grow without limit during failure.
Keep original attempts separate from retries
Budget accounting is easier to reason about when the system distinguishes original attempts from retries.
Consider these metrics:
requests.original
requests.retry
requests.success
requests.failureDo not count a retry as a new original request when calculating the budget. Otherwise retry traffic can replenish the very budget that allowed it, creating a feedback loop.
For example, this is conceptually dangerous:
every attempt increases total_requests
retry_limit = total_requests * 10%If retries increase total_requests, they also increase the future retry allowance.
Prefer an accounting base that retries cannot inflate, such as original attempts or successful requests.
A token bucket is a useful implementation model
One way to implement a retry budget is with tokens.
Each allowed retry consumes one token. Normal successful traffic replenishes tokens up to a configured capacity.
Conceptually:
on successful original request:
add refill tokens
cap at maximum
on failed request that is eligible for retry:
if token available:
consume one token
retry
else:
return the original failureThe bucket gives the system both a long-term retry ratio and a bounded short-term burst.
Suppose one successful request adds 0.1 retry tokens. Over time, ten successful requests fund approximately one retry.
A capacity of 100 tokens then allows some burst recovery without permitting unlimited amplification.
This is only one design. A rolling counter can also work. The main requirement is that concurrent callers share or coordinate budget state at the scope where amplification matters.
Choose the scope that matches the failure domain
A retry budget is useful only if it constrains the traffic that reaches the overloaded component.
If every process has an independent generous budget, a deployment with hundreds of processes can still produce a large aggregate retry wave.
Useful scopes include:
- per dependency;
- per destination cluster;
- per client service;
- per tenant when one tenant can dominate traffic;
- per operation when different operations have very different costs.
The right scope depends on where overload occurs.
For example, a cheap metadata lookup and a heavy report-generation request should not necessarily share the same retry allowance. One retry of the expensive operation may consume far more downstream capacity.
A simple policy should remain the default when request costs are similar. Introduce separate budgets only when the traffic classes genuinely have different failure or resource characteristics.
Retry only failures that can plausibly succeed later
A retry budget does not make every failure retryable.
Retrying a permanent validation failure wastes budget and delays the final response.
Classify failures before consuming a retry token.
Examples that may be retryable include:
- temporary connection failures;
- timeouts where the operation is safe to repeat;
- explicit overload responses that indicate a later attempt may succeed;
- transient dependency unavailability.
Examples that are usually not retryable include:
- invalid input;
- authentication or authorization failures;
- deterministic business-rule rejection;
- malformed requests;
- operations that cannot be repeated safely.
Protocol-specific status codes and error types need protocol-specific rules. Do not create a universal rule such as “retry every 5xx response” without understanding the contract of the dependency.
Idempotency determines whether repetition is safe
A retry can arrive after the original request actually completed but before the client received the response.
That creates an important ambiguity:
client sees timeout
|
+--> operation failed?
|
+--> operation succeeded but response was lost?For read-only operations, repetition is often harmless.
For state-changing operations, retrying can duplicate effects unless the operation is idempotent or protected by an idempotency mechanism.
Consider a payment request. If the server charged the account but the response was lost, blindly retrying the same logical operation could charge twice.
A retry budget limits amplification. It does not make unsafe repetition correct.
Before retrying state changes, define how the server recognizes repeated logical operations and how duplicate effects are prevented.
A retry must fit inside the caller’s deadline
A retry that cannot finish before the caller gives up has no user-visible value.
Suppose an operation has 800 ms remaining before its deadline, and the next retry would use:
backoff delay: 300 ms
request timeout: 700 msThat attempt cannot fit inside the remaining time budget.
Starting it anyway consumes resources even though the caller will likely abandon the result.
A useful retry decision therefore considers both budgets:
traffic budget -> are we allowed to add another attempt?
time budget -> is there enough time for another attempt?Both must be satisfied.
This is especially important in request chains. If service A calls B, which calls C, each layer should respect the remaining end-to-end deadline rather than independently starting a full-length retry cycle.
Limit retries at one layer when possible
Nested retries can multiply each other.
Imagine three service layers, each allowing up to three total attempts.
In the worst case, one incoming operation can cause many downstream attempts because an outer retry repeats all inner retry behavior.
The exact amplification depends on where failures occur, but the design risk is structural: retries at several layers compose multiplicatively.
Prefer to retry at the layer that has the best information to make the decision.
That layer should know:
- whether the operation is safe to repeat;
- which failures are transient;
- how much deadline remains;
- which dependency is overloaded;
- whether retry budget remains.
Other layers can propagate errors instead of each adding their own independent retry loop.
Backoff and jitter still matter after budgeting
A retry budget answers whether another attempt may happen. Backoff and jitter answer when it should happen.
Use both.
Without backoff, allowed retries may hit the dependency immediately after the first failure.
Without jitter, many callers with the same deterministic delay can synchronize and create periodic bursts.
A common structure is:
attempt fails
|
classify error
|
check deadline
|
check retry budget
|
compute capped backoff
|
add jitter
|
sleep
|
retryThe precise backoff formula is less important than avoiding synchronized immediate repetition.
Do not let backoff grow beyond the remaining deadline, and do not sleep merely to consume time when another attempt can no longer complete.
Protect the dependency when failures become widespread
A retry budget naturally becomes more restrictive during a large failure because many callers compete for the same limited retry allowance.
That is desirable.
When failures are rare, retries can hide transient faults.
When failures are widespread, the system should stop assuming each request deserves another attempt.
This creates a graceful transition:
healthy system
-> occasional retries help
partial degradation
-> budget limits amplification
severe outage
-> budget exhausts quickly
and callers fail fastFailing fast may sound less reliable, but during an overload event it can preserve capacity for original requests and recovery work. Continuing to retry every failure can reduce the chance that any request succeeds.
Observe retries as their own traffic class
A retry policy should be measurable.
At minimum, track:
original request rate
retry request rate
retry budget utilization
retry attempts denied by budget
success after retry
latency added by retrying
final failures after retriesThese metrics answer different questions.
A high “success after retry” rate may indicate that retries are useful. A high retry rate with little recovered success may indicate wasted load. Frequent budget exhaustion may indicate either an overly strict budget or a dependency that is unhealthy enough that more retries would be harmful.
Measure the dependency as well. If retries rise at the same time as queue depth, timeout rate, or saturation, the policy may be amplifying pressure.
Common mistakes
Treating the maximum attempt count as the budget
“Retry at most three times” limits one request. It does not limit aggregate retry traffic across thousands of requests.
Use a per-request attempt limit and a shared retry budget for different purposes.
Refilling the budget from retry attempts
Retries must not generate new retry allowance. Refill from original or successful traffic according to the chosen model.
Giving every instance an independent large budget
Per-instance budgets can multiply after autoscaling or large deployments. Match the budget scope to the downstream failure domain.
Retrying after the deadline is effectively spent
A retry that cannot finish before the deadline is wasted work.
Assuming a budget makes non-idempotent operations safe
Budgeting controls load, not duplicate side effects.
Using retries to mask persistent capacity problems
Retries are useful for transient faults. They are not a substitute for fixing insufficient capacity, slow dependencies, hot partitions, or deterministic errors.
When a simpler retry policy is enough
Not every system needs a shared token bucket and detailed retry accounting.
A small internal tool with low traffic may be well served by:
- one retry;
- a short randomized delay;
- a strict timeout;
- retry only for clearly transient failures.
A retry budget becomes more valuable when traffic is high enough that simultaneous retries can materially affect a dependency, or when many callers share the same downstream service.
Complexity should follow the risk.
Conclusion
Retries improve reliability only when the extra work they create remains controlled.
Backoff and jitter spread attempts over time. Per-request limits stop one operation from retrying forever. Idempotency makes repetition safe. Deadlines ensure retries still have time to produce value.
A retry budget adds the missing aggregate control: it limits how much recovery traffic callers may create when failures increase.
Treat retries as a scarce resource rather than an automatic response to failure. When the dependency is healthy, the budget has room to absorb transient faults. When the dependency is struggling, the same budget prevents callers from turning a partial failure into a retry storm.