Retry with Exponential Backoff and Jitter (Go)
Retry transient operations with capped exponential delays, full jitter, context cancellation, and a permanent-error sentinel.
Code
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
type RetryConfig struct {
Attempts int
BaseDelay time.Duration
MaxDelay time.Duration
Jitter float64
}
func (c RetryConfig) validate() error {
if c.Attempts < 1 {
return fmt.Errorf("attempts must be at least 1")
}
if c.BaseDelay < 0 || c.MaxDelay < 0 {
return fmt.Errorf("delays must not be negative")
}
if c.Jitter < 0 || c.Jitter > 1 {
return fmt.Errorf("jitter must be between 0 and 1")
}
return nil
}
var errPermanent = errors.New("permanent failure")
func isRetryable(err error) bool {
return !errors.Is(err, errPermanent)
}
func Do(ctx context.Context, cfg RetryConfig, operation func(context.Context) error) error {
if err := cfg.validate(); err != nil {
return err
}
var lastErr error
for attempt := 1; attempt <= cfg.Attempts; attempt++ {
if err := ctx.Err(); err != nil {
return err
}
lastErr = operation(ctx)
if lastErr == nil {
return nil
}
if !isRetryable(lastErr) || attempt == cfg.Attempts {
break
}
shift := attempt - 1
if shift > 6 {
shift = 6
}
delay := cfg.BaseDelay * time.Duration(1<<shift)
if cfg.MaxDelay > 0 && delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
if cfg.Jitter > 0 {
factor := 1 + (rand.Float64()*2-1)*cfg.Jitter
delay = time.Duration(float64(delay) * factor)
}
timer := time.NewTimer(delay)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
return fmt.Errorf("after retries: %w", lastErr)
}
func main() {
cfg := RetryConfig{
Attempts: 4,
BaseDelay: 10 * time.Millisecond,
MaxDelay: 40 * time.Millisecond,
Jitter: 0.2,
}
attempts := 0
err := Do(context.Background(), cfg, func(ctx context.Context) error {
attempts++
if attempts < 3 {
return fmt.Errorf("temporary failure %d", attempts)
}
return nil
})
fmt.Printf("transient attempts=%d err=%v\n", attempts, err)
attempts = 0
err = Do(context.Background(), cfg, func(ctx context.Context) error {
attempts++
return fmt.Errorf("bad request: %w", errPermanent)
})
fmt.Printf("permanent attempts=%d err=%v\n", attempts, err)
}
Report a problem with this exampleOpens a pre-filled issue on GitHub.