Coding problem · Medium · Async & Concurrency
Retry with Exponential Backoff
Medium Practical tasks Most askedSeen at Stripe, AWS, Netflix, Shopify
Implement retry(operation, maxAttempts, baseDelayMs, sleep):
- Call
operation. If it succeeds, return its result. - If it throws a retryable error and attempts remain, call
sleep(delay)and try again. The delay before retry n (1-based) isbaseDelayMs × 2^(n−1): 100, 200, 400, … - After
maxAttemptstotal attempts, rethrow the last error. - Errors of type
NonRetryableError(NonRetryableExceptionin Java) are rethrown immediately, without sleeping.
sleep is injected so tests run instantly and can assert the delays. In JavaScript/TypeScript, operation and sleep may return promises — retry is async.
Example 1
Input: operation fails twice then returns "ok"; maxAttempts = 5, baseDelayMs = 100 Output: "ok", with sleeps [100, 200]
Hints
Hint 1
Loop attempt = 1..maxAttempts; on error decide: non-retryable → throw, last attempt → throw, else sleep and continue.
Hint 2
Production retries also add jitter (randomize delays) to avoid synchronized retry storms.
Write and run your solution
Loading the editor… (JavaScript is required to run code in your browser.)
Solution and explanation
Show the solution
Time O(maxAttempts) calls · Space O(1)
Loop over attempts; on failure, decide in this order: non-retryable → rethrow; out of attempts → rethrow the last error; otherwise sleep base × 2^(attempt−1) and loop. Injecting sleep (and in real code, a clock and a random source for jitter) keeps the function deterministic and testable.
In production also: retry only idempotent operations (or use idempotency keys), cap the maximum delay, add full jitter (random(0, delay)), respect Retry-After, and bound total retries across a call chain (retry budgets) to avoid amplification.
Reference solution
class NonRetryableError extends Error {}
async function retry(operation, maxAttempts, baseDelayMs, sleep) {
for (let attempt = 1; ; attempt++) {
try {
return await operation();
} catch (err) {
if (err instanceof NonRetryableError || attempt >= maxAttempts) throw err;
await sleep(baseDelayMs * 2 ** (attempt - 1));
}
}
}
class NonRetryableError extends Error {}
async function retry<T>(
operation: () => T | Promise<T>,
maxAttempts: number,
baseDelayMs: number,
sleep: (ms: number) => Promise<void>,
): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await operation();
} catch (err) {
if (err instanceof NonRetryableError || attempt >= maxAttempts) throw err;
await sleep(baseDelayMs * 2 ** (attempt - 1));
}
}
}
from typing import Callable, TypeVar
T = TypeVar("T")
class NonRetryableError(Exception):
pass
def retry(operation: Callable[[], T], max_attempts: int, base_delay_ms: int, sleep: Callable[[int], None]) -> T:
attempt = 1
while True:
try:
return operation()
except NonRetryableError:
raise
except Exception:
if attempt >= max_attempts:
raise
sleep(base_delay_ms * 2 ** (attempt - 1))
attempt += 1
<?php
class NonRetryableError extends RuntimeException
{
}
function retry(callable $operation, int $maxAttempts, int $baseDelayMs, callable $sleep): mixed
{
for ($attempt = 1; ; $attempt++) {
try {
return $operation();
} catch (NonRetryableError $e) {
throw $e;
} catch (Throwable $e) {
if ($attempt >= $maxAttempts) {
throw $e;
}
$sleep($baseDelayMs * 2 ** ($attempt - 1));
}
}
}
class NonRetryableException extends RuntimeException {
NonRetryableException(String message) { super(message); }
}
class Retry {
static <T> T retry(java.util.concurrent.Callable<T> operation, int maxAttempts, long baseDelayMs,
java.util.function.LongConsumer sleep) throws Exception {
for (int attempt = 1; ; attempt++) {
try {
return operation.call();
} catch (NonRetryableException e) {
throw e;
} catch (Exception e) {
if (attempt >= maxAttempts) throw e;
sleep.accept(baseDelayMs * (1L << (attempt - 1)));
}
}
}
}
Common mistakes
- Retrying non-idempotent writes without an idempotency key (double charges).
- Sleeping after the final attempt.
- Swallowing the error and returning
undefined/None.
Follow-up questions
- Add full jitter and a max delay cap.
- Respect a server's Retry-After header.
- Wrap it as a decorator (Python) or higher-order function.
More Async & Concurrency problems
- Implement debounce() · Medium
- Run Async Tasks with a Concurrency Limit · Medium
- Thread-Safe Counting with ExecutorService · Medium