backoff

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2025 License: MIT Imports: 3 Imported by: 0

README

backoff

Go Reference Go Report Card Coverage

Yet another Go backoff library, because sometimes you need to retry things and the existing ones didn't quite fit what I needed.

What's in the box?

  • Three different backoff strategies (constant, exponential, decorrelated jitter)
  • Configurable retry limits and timeouts
  • Built-in jitter to avoid the thundering herd problem
  • Zero dependencies (just stdlib)

Installation

go get github.com/alexjoedt/backoff

Basic usage

package main

import (
    "fmt"
    "time"
    "github.com/alexjoedt/backoff"
)

func main() {
    // Start with 100ms, double each time, give up after 5 tries
    b := backoff.NewExponential(100*time.Millisecond, 2.0,
        backoff.WithMaxRetries(5),
        backoff.WithJitter(), // adds some randomness
    )

    for attempt := 1; ; attempt++ {
        err := doSomethingThatMightFail()
        if err == nil {
            fmt.Println("Success!")
            break
        }

        delay, ok := b.Next()
        if !ok {
            fmt.Println("Giving up after", attempt-1, "attempts")
            break
        }

        fmt.Printf("Try %d failed, waiting %v before retry...\n", attempt, delay)
        time.Sleep(delay)
    }
}

func doSomethingThatMightFail() error {
    // Your flaky operation here
    return nil
}

The different strategies

Constant - when you just want to wait the same time each retry
// Wait 500ms between each attempt, try 3 times max
b := backoff.NewConstant(500*time.Millisecond, backoff.WithMaxRetries(3))

for {
    err := doThing()
    if err == nil {
        break // Success!
    }

    delay, ok := b.Next()
    if !ok {
        return fmt.Errorf("still broken after 3 tries: %w", err)
    }
    time.Sleep(delay)
}
Exponential - the classic approach

Gets progressively longer waits. Good for most retry scenarios.

// Start at 100ms, double each time, but don't wait longer than 10s total
b := backoff.NewExponential(100*time.Millisecond, 2.0,
    backoff.WithMaxInterval(10*time.Second),
    backoff.WithMaxElapsed(30*time.Second),
)

// This will try: 100ms, 200ms, 400ms, 800ms, etc.
for {
    err := callAPI()
    if err == nil {
        break
    }

    delay, ok := b.Next()
    if !ok {
        return fmt.Errorf("API still down after 30s: %w", err)
    }
    time.Sleep(delay)
}
Decorrelated Jitter - the fancy one

This one's more random and helps avoid the "thundering herd" problem when lots of clients are retrying at the same time.

// Random waits that grow over time but stay unpredictable
b := backoff.NewDecorrelated(100*time.Millisecond, 3.0,
    backoff.WithMinInterval(50*time.Millisecond),
    backoff.WithMaxInterval(10*time.Second),
    backoff.WithMaxRetries(10),
)

for {
    err := connectToDatabase()
    if err == nil {
        break
    }

    delay, ok := b.Next()
    if !ok {
        return fmt.Errorf("database still unreachable after 10 tries: %w", err)
    }
    time.Sleep(delay)
}

Configuration

You can customize the behavior with these options:

// When to give up
backoff.WithMaxRetries(5)                 // Stop after 5 attempts  
backoff.WithMaxElapsed(30*time.Second)    // Or stop after 30 seconds total

// Control the timing
backoff.WithMinInterval(100*time.Millisecond)  // Never wait less than this
backoff.WithMaxInterval(10*time.Second)        // Never wait more than this

// Add some randomness
backoff.WithJitter()                           // Adds equal jitter
backoff.WithJitterStrategy(&backoff.FullJitter{})  // More random
backoff.WithJitterStrategy(&backoff.NoneJitter{})  // No randomness

// For testing with predictable randomness
source := rand.NewPCG(42, 1024)
backoff.WithRandSource(source)

Jitter explained

No Jitter - Predictable delays

100ms --> 200ms --> 400ms --> 800ms...

Equal Jitter - Half predictable, half random

50-100ms --> 100-200ms --> 200-400ms --> 400-800ms...

Full Jitter - Completely random within bounds

1-100ms --> 1-200ms --> 1-400ms --> 1-800ms...

Decorrelated Jitter - Random but still grows over time

100ms --> random(min, prev*3) --> random(min, prev*3)...

The randomness helps when you have multiple clients hitting the same service, they won't all retry at exactly the same time.

Thread Safety

Heads up: This library isn't thread-safe. Each goroutine should get its own backoff instance.

// Good: Each worker gets its own backoff
func worker(id int) {
    b := backoff.NewExponential(100*time.Millisecond, 2.0)
    // use b in this goroutine...
}

// Bad: Sharing one instance across goroutines  
b := backoff.NewExponential(100*time.Millisecond, 2.0)
for i := 0; i < 10; i++ {
    go func() {
        b.Next() // Race condition!
    }()
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Documentation

Overview

Package backoff provides various backoff strategies for retry mechanisms.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Constant

type Constant struct {
	// contains filtered or unexported fields
}

Constant implements a constant backoff strategy with fixed delay intervals. This strategy returns the same delay duration for each retry attempt.

Use Constant for scenarios where you want predictable, uniform delays between retry attempts. This is useful when you need consistent timing or when working with systems that have specific rate limiting requirements.

func NewConstant

func NewConstant(d time.Duration, opts ...Option) *Constant

NewConstant creates a new constant backoff strategy with the specified interval.

Parameters:

  • d: The fixed delay duration between retry attempts
  • opts: Optional configuration functions (WithMaxRetries, WithMaxElapsed, etc.)

Example:

// 500ms delay with max 3 retries
constant := NewConstant(500*time.Millisecond, WithMaxRetries(3))

// 1 second delay with 30 second total timeout
constant := NewConstant(time.Second, WithMaxElapsed(30*time.Second))

func (*Constant) Next

func (c *Constant) Next() (time.Duration, bool)

Next returns the next delay duration and whether more retries are allowed. For constant backoff, this always returns the same interval duration until maximum retry or elapsed time limits are reached.

Returns:

  • time.Duration: The delay duration (always the configured interval)
  • bool: true if more retries are allowed, false if limits are reached

func (*Constant) Reset

func (c *Constant) Reset()

Reset resets the constant backoff to its initial state. This clears the retry count and elapsed time, allowing the sequence to be reused for a new set of retry attempts.

type Decorrelated

type Decorrelated struct {
	// contains filtered or unexported fields
}

Decorrelated implements a decorrelated jitter backoff strategy. This strategy uses randomized delays to prevent synchronized retry attempts across multiple clients, effectively preventing thundering herd problems.

The algorithm picks a random delay between the minimum interval and (previous_delay * factor), providing both exponential growth characteristics and randomization to spread out retry attempts.

func NewDecorrelated

func NewDecorrelated(initial time.Duration, factor float64, opts ...Option) *Decorrelated

NewDecorrelated creates a new decorrelated jitter backoff strategy.

Parameters:

  • initial: The initial delay duration for the first retry
  • factor: The growth factor for delay calculation (must be > 1.0)
  • opts: Optional configuration functions

If factor <= 1.0, it defaults to 3.0 for effective jitter spread. If no maxInterval is specified, it defaults to 30 seconds.

Example:

// Start at 100ms with 3x growth factor
dcr := NewDecorrelated(100*time.Millisecond, 3.0,
	WithMaxInterval(10*time.Second),
	WithMaxRetries(5))

// With custom min/max bounds
dcr := NewDecorrelated(50*time.Millisecond, 2.5,
	WithMinInterval(10*time.Millisecond),
	WithMaxInterval(5*time.Second))

func (*Decorrelated) Next

func (dcr *Decorrelated) Next() (time.Duration, bool)

Next returns the next decorrelated delay duration. For the first retry, returns the initial duration. For subsequent retries, picks a random duration between minInterval and (previous_delay * factor), bounded by maxInterval.

This randomization helps prevent multiple clients from retrying simultaneously, reducing load spikes on recovering systems.

Returns:

  • time.Duration: The calculated random delay duration
  • bool: true if more retries are allowed, false if limits are reached

func (*Decorrelated) Reset

func (dcr *Decorrelated) Reset()

Reset resets the decorrelated backoff to its initial state. This clears the retry count, elapsed time, and previous delay history.

type EqualJitter

type EqualJitter struct{}

EqualJitter implements a jitter strategy that uses half the calculated delay as a base and adds randomness to the other half. This provides a good balance between maintaining reasonable delay lengths and adding randomization.

Formula: (calculated_delay / 2) + random(0, calculated_delay / 2)

func (EqualJitter) Apply

Apply returns half the input duration plus a random amount up to the other half. This ensures the result is between 50% and 100% of the original duration. If the input duration is <= 0, returns 0.

type Exponential

type Exponential struct {
	// contains filtered or unexported fields
}

Exponential implements an exponential backoff strategy where delays increase exponentially with each retry attempt.

This strategy is effective for handling temporary failures and avoiding overwhelming systems during recovery periods.

func NewExponential

func NewExponential(base time.Duration, factor float64, opts ...Option) *Exponential

NewExponential creates a new exponential backoff strategy.

Parameters:

  • base: The initial delay duration for the first retry
  • factor: The multiplier applied to increase delays (must be > 1.0)
  • opts: Optional configuration functions

If factor <= 1.0, it defaults to 2.0 for proper exponential growth.

Example:

// Start at 100ms, double each time, max 5 seconds
exp := NewExponential(100*time.Millisecond, 2.0,
	WithMaxInterval(5*time.Second))

// With jitter to prevent thundering herd
exp := NewExponential(50*time.Millisecond, 1.5,
	WithJitter(),
	WithMaxRetries(10))

func (*Exponential) Next

func (e *Exponential) Next() (time.Duration, bool)

Next returns the next exponentially increased delay duration. The delay grows exponentially: base, base*factor, base*factor^2, etc.

The calculated delay is subject to:

  • Jitter application (if configured)
  • Min/max interval bounds
  • Overflow protection (capped at math.MaxInt64)

Returns:

  • time.Duration: The calculated delay duration
  • bool: true if more retries are allowed, false if limits are reached

func (*Exponential) Reset

func (e *Exponential) Reset()

Reset resets the exponential backoff to its initial state. This clears the retry count, elapsed time, and current delay calculation.

type FullJitter

type FullJitter struct{}

FullJitter implements a jitter strategy that randomizes the entire delay. The final delay is a random value between 1 and the calculated delay duration. This provides maximum randomization but may result in very short delays.

Formula: random(1, calculated_delay)

func (FullJitter) Apply

func (FullJitter) Apply(d time.Duration, r *rand.Rand) time.Duration

Apply returns a random duration between 1 and the input duration (inclusive). If the input duration is <= 0, returns 0.

type Jitter

type Jitter interface {
	// Apply takes a calculated delay duration and applies jitter using the
	// provided random number generator, returning the final delay to use.
	Apply(d time.Duration, r *rand.Rand) time.Duration
}

Jitter defines the interface for applying randomization to delay durations. Jitter strategies help prevent thundering herd problems by adding randomness to retry attempts, spreading them out over time instead of having all clients retry simultaneously.

type NoneJitter

type NoneJitter struct{}

NoneJitter implements a jitter strategy that applies no randomization. The delay duration is returned unchanged. This is the default jitter strategy when no jitter options are specified.

func (*NoneJitter) Apply

func (nj *NoneJitter) Apply(d time.Duration, _ *rand.Rand) time.Duration

Apply returns the input duration unchanged, providing no jitter.

type Option

type Option func(*options)

Option is a function type used to configure backoff strategies. Options are applied during the creation of backoff instances to customize behavior such as retry limits, jitter, and timing bounds.

func WithJitter

func WithJitter() Option

WithJitter enables equal jitter for the backoff strategy. Equal jitter adds randomness to delay intervals by using half the calculated delay plus a random amount up to the other half. This helps prevent thundering herd problems.

Example:

backoff := NewExponential(100*time.Millisecond, 2.0,
	WithJitter()) // Uses EqualJitter strategy

func WithJitterStrategy

func WithJitterStrategy(j Jitter) Option

WithJitterStrategy sets a custom jitter strategy for the backoff. This allows you to use FullJitter, EqualJitter, NoneJitter, or implement your own custom jitter algorithm.

Example:

backoff := NewExponential(100*time.Millisecond, 2.0,
	WithJitterStrategy(&FullJitter{}))

func WithMaxElapsed

func WithMaxElapsed(d time.Duration) Option

WithMaxElapsed sets the maximum total elapsed time for all retry attempts. Once this duration has passed, Next() will return (0, false). A value of 0 means no time limit.

Example:

backoff := NewExponential(100*time.Millisecond, 2.0,
	WithMaxElapsed(30*time.Second)) // Stop after 30 seconds total

func WithMaxInterval

func WithMaxInterval(d time.Duration) Option

WithMaxInterval sets the maximum delay interval for backoff strategies. Delays will be capped at this duration regardless of the backoff algorithm. A value of 0 means no maximum limit.

Example:

backoff := NewExponential(100*time.Millisecond, 2.0,
	WithMaxInterval(5*time.Second))

func WithMaxRetries

func WithMaxRetries(v int) Option

WithMaxRetries sets the maximum number of retry attempts. After this many retries, Next() will return (0, false). A value of -1 means unlimited retries.

Example:

backoff := NewConstant(100*time.Millisecond,
	WithMaxRetries(5)) // Stop after 5 attempts

func WithMinInterval

func WithMinInterval(d time.Duration) Option

WithMinInterval sets the minimum delay interval for backoff strategies. Delays will never be shorter than this duration. A value of 0 means no minimum limit.

Example:

backoff := NewExponential(10*time.Millisecond, 2.0,
	WithMinInterval(50*time.Millisecond))

func WithRandSource

func WithRandSource(s rand.Source) Option

WithRandSource sets a custom random source for jitter calculations. This allows for deterministic testing or custom randomization behavior. If not specified, a default PCG source with fixed seed is used.

Example:

source := rand.NewPCG(42, 1024)
backoff := NewExponential(100*time.Millisecond, 2.0,
	WithRandSource(source),
	WithJitter())

type Sequence

type Sequence interface {
	// Next returns the next delay duration and a boolean indicating
	// whether more retries are allowed. Returns (0, false) when
	// maximum retries or elapsed time limits are reached.
	Next() (time.Duration, bool)

	// Reset resets the backoff sequence to its initial state,
	// clearing retry count and elapsed time.
	Reset()
}

Sequence defines the interface for backoff strategies. Implementations should provide methods to get the next delay duration and reset the internal state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL