Building Resilient APIs with Retry Patterns
When your API calls a third-party service, things will go wrong. Networks fail, services go down, rate limits kick in. The question isn't if — it's how gracefully you handle it.
Exponential Backoff
The simplest retry pattern. Wait longer between each attempt:
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, i);
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}
Add jitter to prevent thundering herd problems — don't have all your retries hit the same second.
Circuit Breakers
If a service is down, stop hammering it. A circuit breaker tracks failures and "opens" after a threshold, fast-failing subsequent requests instead of waiting for timeouts.
Three states: Closed (normal), Open (failing fast), Half-Open (testing recovery).
Idempotent Endpoints
Every retry-able endpoint must be idempotent. Use idempotency keys — the client sends a unique key, and the server deduplicates based on it.
The pattern that saved us in production: store the idempotency key + response in Redis with a TTL. If the same key arrives again, return the cached response.