Luke Oliff.

TIL: Implementing Exponential Backoff in TypeScript

·TIL·1 min read·Luke Oliff

APIs fail. Networks glitch. The right response is to retry with increasing delays.

async function fetchWithRetry(url: string, maxRetries = 3): Promise<Response> {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url)
    if (response.ok) return response
    const delay = Math.min(1000 * Math.pow(2, i) + Math.random() * 1000, 10000)
    console.log(`Retry ${i + 1} after ${delay}ms`)
    await new Promise(r => setTimeout(r, delay))
  }
  throw new Error('Max retries exceeded')
}

Delay doubles each attempt: 1s, 2s, 4s. Random jitter prevents thundering herd when multiple clients retry simultaneously. The cap at 10s stops the delay growing unbounded.

const result = await fetchWithRetry('https://api.example.com/v1/listen')

The function looks like a normal call. The caller does not need to know about retry logic.

What status codes should I retry on?

429 (rate limited) and 5xx (server errors). Do not retry 4xx errors like 400 or 401.

Should I log retries?

Yes. Log each attempt with the attempt number and delay. Debugging flaky integrations is much easier with that trace.