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.