go / backoff
I use a lookup table for retry backoff instead of computing delays with exponential math.
The pattern
Josh Bleecher Snyder's Simpler backoff makes the case for replacing computed backoff with an explicit delay table.
Instead of:
delay := baseDelay
for attempt := range maxAttempts {
err := request(ctx)
if err == nil {
return nil
}
delay *= 2
delay = min(delay, maxDelay)
time.Sleep(delay)
}
Use:
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.