or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

backoff-policies.mdindex.mdretry-operations.mdticker.md
tile.json

retry-operations.mddocs/

Retry Operations

Retry operations provide functions to automatically retry failed operations using backoff policies. All retry functions guarantee at least one execution attempt and handle permanent errors that should not be retried.

Operation Types

// Function type for operations executed by Retry() or RetryNotify()
// The operation will be retried using a backoff policy if it returns an error
type Operation func() error

// Generic function type for operations that return data along with error
// Used by RetryWithData() and RetryNotifyWithData()
type OperationWithData[T any] func() (T, error)

Basic Retry Functions

Retry

Retries an operation until it succeeds or the backoff policy stops.

// Retry the operation until it does not return error or BackOff stops
// o is guaranteed to be run at least once
// If o returns a *PermanentError, the operation is not retried
// Sleeps the goroutine for the duration returned by BackOff after failure
func Retry(o Operation, b BackOff) error

Usage Example

import "github.com/cenkalti/backoff/v4"

operation := func() error {
    // Your operation that may fail
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        return fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }

    return nil
}

err := backoff.Retry(operation, backoff.NewExponentialBackOff())
if err != nil {
    log.Fatal(err)
}

RetryWithData

Like Retry but returns data from the operation along with any error.

// RetryWithData is like Retry but returns data in the response too
func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error)

Usage Example

import "github.com/cenkalti/backoff/v4"

type APIResponse struct {
    Data string
    Code int
}

operation := func() (APIResponse, error) {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        return APIResponse{}, err
    }
    defer resp.Body.Close()

    // Process response
    var result APIResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return APIResponse{}, err
    }

    return result, nil
}

result, err := backoff.RetryWithData(operation, backoff.NewExponentialBackOff())
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Got data: %+v\n", result)

Retry with Notifications

Notify Callback Type

// Notify is a callback function invoked on each retry attempt
// Receives the operation error and backoff delay
// NOTE: If backoff policy stops retrying, notify is not called
type Notify func(error, time.Duration)

RetryNotify

Retries with a callback invoked before each sleep.

// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep
func RetryNotify(operation Operation, b BackOff, notify Notify) error

Usage Example

import (
    "log"
    "github.com/cenkalti/backoff/v4"
)

notifyFunc := func(err error, duration time.Duration) {
    log.Printf("Error occurred: %v. Retrying in %v", err, duration)
}

operation := func() error {
    // Your operation
    return someAPICall()
}

err := backoff.RetryNotify(
    operation,
    backoff.NewExponentialBackOff(),
    notifyFunc,
)

RetryNotifyWithData

Like RetryNotify but returns data from the operation.

// RetryNotifyWithData is like RetryNotify but returns data in the response too
func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error)

Usage Example

import (
    "log"
    "github.com/cenkalti/backoff/v4"
)

type Result struct {
    Value string
}

notifyFunc := func(err error, duration time.Duration) {
    log.Printf("Retry attempt failed: %v. Waiting %v", err, duration)
}

operation := func() (Result, error) {
    // Your operation that returns data
    return fetchData()
}

result, err := backoff.RetryNotifyWithData(
    operation,
    backoff.NewExponentialBackOff(),
    notifyFunc,
)
if err != nil {
    log.Fatal(err)
}

Retry with Custom Timer

For advanced use cases requiring custom timer implementations (e.g., testing).

Timer Interface

// Timer interface for custom timer implementations
type Timer interface {
    // Start the timer to fire after the given duration
    // Can be called multiple times to reset the timer
    Start(duration time.Duration)

    // Stop the timer and free resources
    Stop()

    // C returns the channel that receives the time when timer fires
    // Should return the same channel instance across calls
    C() <-chan time.Time
}

RetryNotifyWithTimer

// RetryNotifyWithTimer calls notify function with the error and wait duration
// using the given Timer for each failed attempt before sleep
// A default timer using system timer is used when nil is passed
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error

Usage Example

import "github.com/cenkalti/backoff/v4"

// Use default timer (pass nil)
err := backoff.RetryNotifyWithTimer(
    operation,
    backoff.NewExponentialBackOff(),
    notifyFunc,
    nil, // Uses default timer
)

// Or provide custom timer implementation
customTimer := &MyCustomTimer{}
err := backoff.RetryNotifyWithTimer(
    operation,
    backoff.NewExponentialBackOff(),
    notifyFunc,
    customTimer,
)

RetryNotifyWithTimerAndData

// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data
// in the response too
func RetryNotifyWithTimerAndData[T any](
    operation OperationWithData[T],
    b BackOff,
    notify Notify,
    t Timer,
) (T, error)

Permanent Errors

Permanent errors signal that an operation should not be retried, regardless of the backoff policy.

PermanentError Type

// PermanentError signals that the operation should not be retried
type PermanentError struct {
    Err error
}

// Returns the error string from wrapped error
func (e *PermanentError) Error() string

// Returns the wrapped error
func (e *PermanentError) Unwrap() error

// Checks if target is a PermanentError
func (e *PermanentError) Is(target error) bool

Permanent Function

// Permanent wraps the given err in a *PermanentError
// Returns nil if err is nil
func Permanent(err error) error

Usage Examples

Marking Non-Retryable Errors

import (
    "errors"
    "github.com/cenkalti/backoff/v4"
)

operation := func() error {
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        return err // Transient error, will retry
    }
    defer resp.Body.Close()

    switch resp.StatusCode {
    case 200:
        return nil // Success
    case 400, 401, 403, 404:
        // Client errors - don't retry
        return backoff.Permanent(
            fmt.Errorf("client error: %d", resp.StatusCode),
        )
    case 429, 500, 502, 503:
        // Server errors - retry
        return fmt.Errorf("server error: %d", resp.StatusCode)
    default:
        return fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }
}

err := backoff.Retry(operation, backoff.NewExponentialBackOff())
// Will immediately return on 400/401/403/404 without retrying
// Will retry on 429/500/502/503 and other errors

Handling nil Errors

import "github.com/cenkalti/backoff/v4"

operation := func() error {
    result, err := performOperation()
    if err != nil {
        // Check for specific error conditions
        if isClientError(err) {
            return backoff.Permanent(err)
        }
        return err
    }

    // Success case - returning nil
    return nil
}

// Permanent(nil) returns nil, so this is safe
err := backoff.Retry(operation, backoff.NewExponentialBackOff())

Checking for Permanent Errors

import (
    "errors"
    "github.com/cenkalti/backoff/v4"
)

err := backoff.Retry(operation, b)
if err != nil {
    var permanent *backoff.PermanentError
    if errors.As(err, &permanent) {
        log.Printf("Permanent failure: %v", permanent.Err)
    } else {
        log.Printf("Retry limit exceeded: %v", err)
    }
}

Context Integration

All retry functions automatically respect context cancellation when using WithContext wrapper.

Usage Example

import (
    "context"
    "time"
    "github.com/cenkalti/backoff/v4"
)

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

operation := func() error {
    // Your operation
    return performTask()
}

b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
err := backoff.Retry(operation, b)

if err != nil {
    if ctx.Err() != nil {
        log.Printf("Stopped due to context: %v", ctx.Err())
    } else {
        log.Printf("Operation failed: %v", err)
    }
}

Best Practices

1. Use Context for Timeouts

Always use WithContext to prevent indefinite retries:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

b := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
err := backoff.Retry(operation, b)

2. Limit Retry Attempts

Combine backoff policies with WithMaxRetries for circuit breaker patterns:

b := backoff.WithMaxRetries(
    backoff.NewExponentialBackOff(),
    5, // Maximum 5 attempts
)

3. Mark Permanent Failures

Wrap non-retryable errors with Permanent():

if isClientError(err) {
    return backoff.Permanent(err)
}
return err // Will retry

4. Monitor Retry Attempts

Use RetryNotify to log or monitor retry attempts:

notifyFunc := func(err error, duration time.Duration) {
    metrics.IncrementRetryCount()
    log.Printf("Retry #%d after error: %v", attemptCount, err)
}

err := backoff.RetryNotify(operation, b, notifyFunc)

5. Configure Appropriate Intervals

Adjust backoff parameters based on your use case:

// For fast-failing APIs
b := backoff.NewExponentialBackOff(
    backoff.WithInitialInterval(100*time.Millisecond),
    backoff.WithMaxInterval(5*time.Second),
    backoff.WithMaxElapsedTime(30*time.Second),
)

// For slow external services
b := backoff.NewExponentialBackOff(
    backoff.WithInitialInterval(2*time.Second),
    backoff.WithMaxInterval(2*time.Minute),
    backoff.WithMaxElapsedTime(15*time.Minute),
)

Common Patterns

API Client with Retry

type APIClient struct {
    baseURL string
    backoff backoff.BackOff
}

func (c *APIClient) Get(ctx context.Context, path string) ([]byte, error) {
    var result []byte

    operation := func() error {
        req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+path, nil)
        if err != nil {
            return backoff.Permanent(err)
        }

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        if resp.StatusCode >= 400 && resp.StatusCode < 500 {
            return backoff.Permanent(fmt.Errorf("client error: %d", resp.StatusCode))
        }

        if resp.StatusCode != 200 {
            return fmt.Errorf("server error: %d", resp.StatusCode)
        }

        result, err = io.ReadAll(resp.Body)
        return err
    }

    b := backoff.WithContext(c.backoff, ctx)
    err := backoff.Retry(operation, b)
    return result, err
}

Database Connection with Retry

func ConnectWithRetry(ctx context.Context, dsn string) (*sql.DB, error) {
    var db *sql.DB

    operation := func() error {
        var err error
        db, err = sql.Open("postgres", dsn)
        if err != nil {
            return err
        }

        // Verify connection
        if err := db.PingContext(ctx); err != nil {
            db.Close()
            return err
        }

        return nil
    }

    b := backoff.WithContext(
        backoff.NewExponentialBackOff(
            backoff.WithMaxElapsedTime(1*time.Minute),
        ),
        ctx,
    )

    err := backoff.Retry(operation, b)
    return db, err
}

Implementation Notes

  • Guaranteed Execution: All retry functions execute the operation at least once, even if backoff immediately returns Stop
  • Goroutine Sleeping: Retry functions sleep the calling goroutine between attempts
  • Context Checking: Context cancellation is checked before sleeping, preventing unnecessary waits
  • Error Wrapping: Original errors are returned as-is; permanent errors return the wrapped error
  • Notify Timing: Notify callback is called before sleeping, not after