Skip to content
Back to blog

Building Resilient APIs with Retry Patterns

#architecture#node.js

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.