A Go package providing composable retry delay strategies based on the recommendations from the AWS Architecture Blog.
When a service call fails, naive retry logic that immediately retries or retries at fixed intervals can make things worse under load. This package provides several well-known backoff strategies that help distribute retry pressure across clients, reducing contention and improving system resilience.
go get github.com/spacemagneto/gobackoff| Constant | Value | Description |
|---|---|---|
DefaultMinBackoff |
1s |
Starting delay if strategy is initialized with <= 0 |
DefaultMaxBackoff |
20s |
Upper bound — no strategy returns a value exceeding this |
DefaultStep |
2.0 |
Base factor for exponential growth |
type Backoff interface {
Next(arg int64) time.Duration
}All strategies implement the Backoff interface. The meaning of the arg parameter depends on the strategy:
| Strategy | arg meaning |
|---|---|
Exponential |
Retry attempt number (1, 2, 3...) |
ExponentialJitter |
Retry attempt number (1, 2, 3...) |
FullJitter |
Retry attempt number (1, 2, 3...) |
EqualJitter |
Retry attempt number (1, 2, 3...) |
DecorrelatedJitter |
Previous delay in nanoseconds (time.Duration.Nanoseconds()) |
Fixed |
Ignored |
Always returns the same delay regardless of attempts or state. Useful for simple polling or scenarios where growth is not desired.
Formula: sleep = delay
b := backoff.NewFixed(2 * time.Second)
b.Next(1) // 2s
b.Next(2) // 2s
b.Next(3) // 2sA deterministic exponential backoff without any randomness. The delay grows by a fixed multiplier on every attempt. Predictable, but susceptible to thundering herd under high concurrency — consider a jitter strategy instead.
Formula: sleep = min(cap, base * step^attempt)
b := backoff.NewExponential(1*time.Second, 20*time.Second)
// or with a custom step:
b := backoff.NewExponentialWithStep(1*time.Second, 20*time.Second, 3.0)
b.Next(1) // 2s
b.Next(2) // 4s
b.Next(3) // 8sExponential backoff with full jitter. Combines the gradual back-pressure of exponential growth with the desynchronization benefits of randomness. Unlike FullJitter (which always uses base 2 as the multiplier), this strategy accepts a configurable step — making it a flexible default for most retry scenarios.
Formula: sleep = random_between(0, min(cap, base * step^attempt))
b := backoff.NewExponentialJitter(1*time.Second, 20*time.Second)
// or with a custom growth factor:
b := backoff.NewExponentialJitterWithStep(1*time.Second, 20*time.Second, 1.5)
b.Next(1) // random in [0s, 2s]
b.Next(2) // random in [0s, 4s]
b.Next(3) // random in [0s, 8s]Exponential backoff with a fully randomized delay. For each attempt the maximum possible delay is computed, and a random value in [0, max] is returned. Provides the best protection against thundering herd by maximizing desynchronization between clients.
Formula: sleep = random_between(0, min(cap, base * 2^attempt))
b := backoff.NewFullJitter(1*time.Second, 20*time.Second)
b.Next(1) // random in [0s, 2s]
b.Next(2) // random in [0s, 4s]
b.Next(3) // random in [0s, 8s]Exponential backoff where the delay is always at least half of the current exponential limit. Provides a more predictable lower bound than Full Jitter while still preventing client synchronization.
Formula:
temp = min(cap, base * 2^attempt)
sleep = temp/2 + random_between(0, temp/2)
b := backoff.NewEqualJitter(1*time.Second, 20*time.Second)
b.Next(1) // random in [1s, 2s]
b.Next(2) // random in [2s, 4s]
b.Next(3) // random in [4s, 8s]A state-aware strategy where each delay is derived from the previous delay rather than the attempt number. This produces a "random walk" behavior with excellent desynchronization properties. AWS recommends this alongside Full Jitter as one of the most effective strategies.
Formula: sleep = min(cap, random_between(base, previous_sleep * 3))
Note: Because this strategy is state-dependent, pass
0on the first call and the result of the previous call's.Nanoseconds()on subsequent calls.
b := backoff.NewDecorrelatedJitter(1*time.Second, 20*time.Second)
var prev int64
for i := 0; i < 5; i++ {
d := b.Next(prev)
prev = d.Nanoseconds()
fmt.Println(d)
}| Strategy | Jitter | State-aware | Lower bound guaranteed | Best for |
|---|---|---|---|---|
Fixed |
✗ | ✗ | ✓ | Simple polling |
Exponential |
✗ | ✗ | ✓ | Baseline / non-concurrent scenarios |
ExponentialJitter |
✓ | ✗ | ✗ | General purpose — configurable growth |
FullJitter |
✓ | ✗ | ✗ | High-concurrency, thundering herd |
EqualJitter |
✓ | ✗ | ✓ | Balance between predictability and spread |
DecorrelatedJitter |
✓ | ✓ | ✓ | Large-scale distributed systems |
package main
import (
"fmt"
"time"
"github.com/spacemagneto/gobackoff"
)
func main() {
b := backoff.NewFullJitter(500*time.Millisecond, 30*time.Second)
for attempt := int64(1); attempt <= 5; attempt++ {
delay := b.Next(attempt)
fmt.Printf("Attempt %d: waiting %s\n", attempt, delay)
time.Sleep(delay)
// ... perform operation
}
}