go / backoff

I use a lookup table for retry backoff.

Pattern

The computed version:

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

The table version:

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 table version has fewer variables and keeps no state between iterations. A schedule change is one edit.

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
}

Limits

The computed version wins only when the delay sequence is unbounded or the program picks it at runtime.

← All articles