Skip to main content

Overview

The retry package provides a robust retry mechanism with exponential backoff and full jitter, designed to gracefully handle transient errors in distributed systems. It supports context cancellation and flexible configuration options.

Import

import "github.com/yourusername/gotemplate/pkg/retry"

Functions

Do

func Do(ctx context.Context, fn func(ctx context.Context) error, opts ...Option) error
Executes a function with automatic retry logic. It calls fn repeatedly until it returns nil, the context is cancelled, or maxAttempts is reached. Between attempts, it sleeps for an exponentially increasing duration with full jitter.
ctx
context.Context
required
Context for cancellation and timeouts. When cancelled, Do returns immediately with ctx.Err().
fn
func(ctx context.Context) error
required
The function to execute and retry on error. Should return nil on success or an error to trigger a retry.
opts
...Option
Optional configuration options to customize retry behavior (see Options section).
Returns: nil if fn succeeds, ctx.Err() if context is cancelled, or a wrapped error after all attempts are exhausted. Backoff Algorithm: Between retries, sleeps for a randomized duration: rand([0, min(initialDelay * multiplier^attempt, maxDelay)])

Example

ctx := context.Background()

err := retry.Do(ctx, func(ctx context.Context) error {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        return err // Will retry
    }
    defer resp.Body.Close()
    
    if resp.StatusCode >= 500 {
        return fmt.Errorf("server error: %d", resp.StatusCode) // Will retry
    }
    
    return nil // Success
}, retry.WithMaxAttempts(5), retry.WithInitialDelay(2*time.Second))

if err != nil {
    log.Fatalf("Failed after retries: %v", err)
}

Example with Context Timeout

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := retry.Do(ctx, func(ctx context.Context) error {
    return performDatabaseOperation(ctx)
}, 
    retry.WithMaxAttempts(10),
    retry.WithInitialDelay(500*time.Millisecond),
    retry.WithMaxDelay(5*time.Second),
    retry.WithMultiplier(1.5),
)

if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        log.Println("Operation timed out")
    } else {
        log.Printf("Operation failed: %v", err)
    }
}

Options

WithMaxAttempts

func WithMaxAttempts(n int) Option
Sets the maximum number of attempts (including the first execution). A value of 1 means no retries.
n
int
required
Maximum number of attempts. Must be at least 1.
Default: 3

Example

// Retry up to 5 times total
retry.Do(ctx, fn, retry.WithMaxAttempts(5))

// No retries, execute once only
retry.Do(ctx, fn, retry.WithMaxAttempts(1))

WithInitialDelay

func WithInitialDelay(d time.Duration) Option
Sets the base delay before the first retry. Subsequent delays grow exponentially from this base.
d
time.Duration
required
Initial delay duration before the first retry.
Default: 1 * time.Second

Example

// Start with a 100ms delay
retry.Do(ctx, fn, retry.WithInitialDelay(100*time.Millisecond))

// Start with a 5 second delay
retry.Do(ctx, fn, retry.WithInitialDelay(5*time.Second))

WithMaxDelay

func WithMaxDelay(d time.Duration) Option
Sets the maximum delay cap for backoff. Once the exponential delay exceeds this value, it will be clamped to maxDelay.
d
time.Duration
required
Maximum delay duration between retries.
Default: 10 * time.Second

Example

// Cap delays at 30 seconds
retry.Do(ctx, fn, retry.WithMaxDelay(30*time.Second))

// Cap delays at 1 minute
retry.Do(ctx, fn, retry.WithMaxDelay(time.Minute))

WithMultiplier

func WithMultiplier(m float64) Option
Sets the exponential growth factor for backoff delays. Each retry’s delay is multiplied by this factor.
m
float64
required
Exponential multiplier for delay growth. Common values are 1.5-3.0.
Default: 2.0

Example

// More aggressive backoff (doubles each time)
retry.Do(ctx, fn, retry.WithMultiplier(2.0))

// Gentler backoff (1.5x each time)
retry.Do(ctx, fn, retry.WithMultiplier(1.5))

// Very aggressive backoff (triples each time)
retry.Do(ctx, fn, retry.WithMultiplier(3.0))

Types

Option

type Option func(*config)
A function type that configures retry behavior. Options are passed to Do() to customize retry parameters.

Constants

The following default values are used when options are not explicitly provided:
  • defaultMaxAttempts: 3 - Maximum number of attempts
  • defaultInitialDelay: 1 * time.Second - Base delay before first retry
  • defaultMaxDelay: 10 * time.Second - Maximum delay cap
  • defaultMultiplier: 2.0 - Exponential growth factor

Complete Example

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "time"
    
    "github.com/yourusername/gotemplate/pkg/retry"
)

var ErrTransient = errors.New("transient error")

func main() {
    ctx := context.Background()
    
    // Example: Retry with custom configuration
    attempt := 0
    err := retry.Do(ctx, func(ctx context.Context) error {
        attempt++
        fmt.Printf("Attempt %d\n", attempt)
        
        // Simulate transient failures
        if attempt < 3 {
            return ErrTransient
        }
        
        // Success on third attempt
        return nil
    },
        retry.WithMaxAttempts(5),
        retry.WithInitialDelay(500*time.Millisecond),
        retry.WithMaxDelay(10*time.Second),
        retry.WithMultiplier(2.0),
    )
    
    if err != nil {
        log.Fatalf("Operation failed: %v", err)
    }
    
    fmt.Println("Operation succeeded!")
}

Error Handling

Success

When fn returns nil, Do() returns immediately with nil.

Context Cancellation

When the context is cancelled during a retry delay, Do() returns ctx.Err() immediately.

Exhausted Attempts

After all attempts are exhausted, Do() returns a wrapped error:
after N attempts: <last error>
You can unwrap the original error using errors.Unwrap() or check with errors.Is().

Best Practices

  1. Use Context Timeouts: Always provide a context with timeout to prevent infinite retries
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
    defer cancel()
    
  2. Configure for Your Use Case: Adjust retry parameters based on your needs
    • Fast operations: shorter delays, fewer attempts
    • External APIs: longer delays, more attempts
    • Critical operations: more attempts with exponential backoff
  3. Distinguish Error Types: Only retry transient errors
    retry.Do(ctx, func(ctx context.Context) error {
        err := operation()
        if isPermanentError(err) {
            return nil // Don't retry permanent errors
        }
        return err
    })
    
  4. Log Retry Attempts: Add logging to track retry behavior
    attempt := 0
    retry.Do(ctx, func(ctx context.Context) error {
        attempt++
        err := operation()
        if err != nil {
            log.Printf("Attempt %d failed: %v", attempt, err)
        }
        return err
    })
    

Backoff Behavior

The retry package uses exponential backoff with full jitter:
  • Exponential: Each delay grows by the multiplier factor
  • Full Jitter: Random value between 0 and the calculated delay
  • Capped: Delays never exceed maxDelay

Example Delays (with default settings)

AttemptCalculationRangeAverage
12^0 * 1s0-1s500ms
22^1 * 1s0-2s1s
32^2 * 1s0-4s2s
42^3 * 1s0-8s4s
52^4 * 1s0-10s (capped)5s
Benefits of Full Jitter:
  • Reduces thundering herd problem
  • Spreads load more evenly across time
  • Prevents synchronized retries from multiple clients

Build docs developers (and LLMs) love