go / backoff

I use a lookup table for retry backoff instead of computing delays with exponential math.

The pattern

Instead of computing backoff:

delay := baseDelay
for attempt := range maxAttempts {
    err := request(ctx)
    if err == nil {
        return nil
    }
    delay *= 2
    delay = min(delay, maxDelay)
    time.Sleep(delay)
}

Use an explicit delay table:

delays := []time.Duration{
    1 * time.Second,
    2 * time.Second,
    4 * time.Second,
    8 * time.Second,
    16 * time.Second,
}
for _, delay := range delays {
    err := request(ctx)
    if err == nil {
        return nil
    }
    time.Sleep(delay)
}

The lookup table version has fewer variables, smaller scope, and no cross-iteration state to reason about. It's also easier to edit. Changing the schedule feels safe and trivial.

Example

A retry loop that backs off on 404s from an eventually-consistent API:

var retryDelays = []time.Duration{
    1 * time.Second,
    2 * time.Second,
    4 * time.Second,
    8 * time.Second,
    16 * time.Second,
}

func fetchFiles(pr int) ([]File, error) {
    var files []File
    err := api.Get(&files, "pulls/%d/files", pr)

    for _, delay := range retryDelays {
        if err != ErrNotFound {
            break
        }
        time.Sleep(delay)
        err = api.Get(&files, "pulls/%d/files", pr)
    }

    return files, err
}

When to use

Most retry scenarios that I have run into fit this pattern. The computed version only wins when the delay sequence is truly unbounded or determined at runtime.

See sleepctx for context-aware delays.

← All articles