Problem
Transient faults such as brief network errors or momentary throttling are common in distributed systems. Naive immediate retries multiply load on struggling services and can turn a small blip into an outage.
Recover from transient failures by retrying calls safely without amplifying overload.
Transient faults such as brief network errors or momentary throttling are common in distributed systems. Naive immediate retries multiply load on struggling services and can turn a small blip into an outage.
Retrying is the simplest resilience technique and the easiest to misuse. Done well, it hides transient noise. Done badly, it multiplies traffic exactly when a dependency can least afford it.
After a failure classified as retryable, the client waits for a delay computed as the base interval times two to the power of the attempt, capped at a maximum, then randomized with jitter. It stops when the attempt count or total deadline is reached. A retry budget, such as ten percent of recent requests, prevents retries from dominating traffic. If the response includes a Retry-After header, the client honors it. Only the outermost layer that has context about the user request performs the retry, avoiding multiplication across tiers.
// Exponential backoff with full jitter
async function withRetry(fn, { max = 4, base = 100, cap = 5000 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
if (!err.retryable || attempt >= max) throw err;
const delay = Math.random() * Math.min(cap, base * 2 ** attempt);
await new Promise((r) => setTimeout(r, delay));
}
}
}
Teams adopt this approach when calls fail transiently due to network or throttling; operations are idempotent or protected by idempotency keys. It is a poor fit when failures are permanent, such as validation or authorization errors, or when operations are non-idempotent and cannot be keyed. The central tensions are success rate vs added load; persistence vs latency; client autonomy vs system-wide retry budgets. The main benefits are that it delivers: masks brief faults from users; simple to implement with libraries; improves success rates for flaky networks; backoff and jitter reduce synchronized load. The main costs to budget for are: amplifies load during widespread failures; adds latency to failing requests; risk of duplicate effects without idempotency; layered retries multiply exponentially. Governance typically looks like this: shared client libraries encode approved retry defaults. Expect adoption cost to be low: library configuration and policy, and a best fit for any team making remote calls. Critics point out that retries are a leading cause of metastable failures and retry storms, and that defaults copied between services often ignore each dependency's behavior. Treat these concerns as design inputs, and revisit the decision as traffic, team size, and failure history change.