Skip to content

7 min read

Retry Logic and Backoff Patterns: Building Resilience

Published 2026-09-06 · Updated 2026-09-06

Production ReliabilityNode.jsAPI IntegrationError Handling
Timeline diagram showing exponential backoff intervals increasing between retry attempts

Learn when and how to implement exponential backoff and retry strategies in production systems without overcomplicating your architecture.

Transient failures happen everywhere in production: network timeouts, rate-limited third-party APIs, temporary database connections hiccups, and overloaded services. Your first instinct—immediate retry—usually makes things worse. A coordinated flood of retries from hundreds of clients can exhaust quota, trigger cascading failures, and extend the outage. The solution isn't complicated, but it requires deliberate choice. This article covers the three patterns that actually work: simple exponential backoff for external API calls, jitter to prevent thundering herds, and circuit breakers to stop throwing requests at a failing service.

Exponential backoff means doubling (or multiplying by a fixed factor) the wait time between retries: first attempt immediately, second after 1 second, third after 2 seconds, fourth after 4 seconds. After 5–6 retries, you've waited 30+ seconds, which covers most transient issues and gives overloaded services time to recover. The math is simple: `delay = baseDelay * (multiplier ^ attempt)`. Without exponentiation, linear backoff (1, 2, 3, 4 seconds) recovers too slowly; immediate retries create a denial-of-service pattern that harms the very service you depend on. Jitter—a randomized amount added to each delay—prevents synchronized retries across multiple clients from overwhelming a recovering service. If 1,000 clients all retry after exactly 2 seconds, the server sees a synchronized spike. Add jitter (e.g., ±20% of the delay), and retries spread across a window, distributing load smoothly.

A practical pattern for Node.js: set a base delay of 1 second, multiply by 2 on each retry, cap at 30 seconds, include 20% random jitter, and fail after 5 retries. For quota-limited APIs (like Stripe, OpenAI), adjust the base to match the documented rate limit window—don't guess. For internal services and databases, shorter timeouts (50ms base) with fewer retries work better. A circuit breaker wraps this logic: after N failures in a time window (e.g., 5 failures in 30 seconds), stop retrying and fail fast, returning an error or fallback response. This prevents wasting time and resources on a service that's clearly down. Most teams either skip circuit breakers (letting retries pile up) or implement them wrong (hardcoding thresholds that don't match their traffic patterns). The tradeoff: circuit breakers add latency to the first request after recovery (it probes the service), but prevent hours of degradation from a slow cascade.

Start by implementing exponential backoff for all external API calls and database connections—this single pattern solves 80% of real-world transient failures. Use a library (like `async-retry` for Node.js) rather than rolling your own; hand-coded retry logic is a common source of subtle bugs. Add jitter immediately; it costs nothing and prevents coordinated failures. Implement circuit breakers only when you can't lose a request (use an async job queue or cache instead) or when you have confirmed that retries are causing cascading failures. Monitor retry rates and failure patterns in your logs; if retries are consistently high (>5% of requests), the issue isn't transient, and retrying won't help. Finally, test your retry logic under realistic failure conditions (network timeout, 500 error, rate limit) before deploying—the production behavior of retries is often surprising.