go 33 lines · 1 tab

Exponential backoff with jitter for retries

Leah Thompson Jan 2026
1 tab
package retry

import (
  "context"
  "math/rand"
  "time"
)

func Retry(ctx context.Context, attempts int, base time.Duration, fn func() error, shouldRetry func(error) bool) error {
  var err error
  for i := 0; i < attempts; i++ {
    if err = fn(); err == nil {
      return nil
    }
    if !shouldRetry(err) {
      return err
    }

    d := base * (1 << i)
    if d > 2*time.Second {
      d = 2 * time.Second
    }
    jitter := time.Duration(rand.Int63n(int64(d / 2)))
    sleep := d/2 + jitter

    select {
    case <-ctx.Done():
      return ctx.Err()
    case <-time.After(sleep):
    }
  }
  return err
}
1 file · go Explain with highlit

Retries are dangerous when they synchronize; that’s how you turn a minor outage into a stampede. I implement exponential backoff with jitter so clients spread out naturally. The Retry helper takes a shouldRetry predicate and always checks ctx.Done() before sleeping, which prevents retries from continuing after the request has already timed out. I also cap the delay to avoid waiting minutes on a hot path. The other detail is what I log: I log attempt number and delay, not the full error payload (which may include secrets). In production this gives you the safety of retries for transient failures while still failing fast under real outages.


Related snips

Share this code

Here's the card — post it anywhere.

Exponential backoff with jitter for retries — share card
Link copied