Network call fails. You retry immediately. It fails again. You retry in a tight loop. You DDoS your own service.
Exponential Backoff
Wait longer between each retry: 1s, 2s, 4s, 8s, 16s.
async function retry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try { return await fn(); }
catch (err) {
if (i === maxRetries - 1) throw err;
const delay = Math.min(1000 * Math.pow(2, i), 30000);
const jitter = delay * (0.5 + Math.random() * 0.5);
await new Promise(r => setTimeout(r, jitter));
}
}
}
Why Jitter Matters
Without jitter, 1000 clients all retry at the same exponential intervals. They stampede the server simultaneously. Jitter randomizes the retry time so clients spread out naturally.
What NOT to Retry
Retry: 429 (rate limit), 503 (overloaded), timeout, connection refused.
Do NOT retry: 400 (bad request), 401 (unauthorized), 404 (not found), 422 (validation error). These will always fail.
Dead Letter Queue
After max retries exhausted, put the failed message in a DLQ. Review and reprocess later. Never silently drop messages.
Part of my Production Backend Patterns series. Follow for more practical backend engineering.
If this was useful, consider:
- Sponsoring on GitHub to support more open-source tools
- Buying me a coffee on Ko-fi
You Might Also Like
- Feature Flags from Scratch: Build a Runtime Toggle System in TypeScript (2026)
- Graceful Degradation Patterns: Keep Your Backend Running When Dependencies Fail (2026)
- Request Validation at the Edge: Zod Schemas, OpenAPI, and Type-Safe APIs (2026)
Follow me for more production-ready backend content!
If this helped you, buy me a coffee on Ko-fi!
Top comments (0)