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.
// 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)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) errorimport "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)
}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)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)// 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)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) errorimport (
"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,
)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)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)
}For advanced use cases requiring custom timer implementations (e.g., testing).
// 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 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) errorimport "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 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 signal that an operation should not be retried, regardless of the backoff policy.
// 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 wraps the given err in a *PermanentError
// Returns nil if err is nil
func Permanent(err error) errorimport (
"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 errorsimport "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())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)
}
}All retry functions automatically respect context cancellation when using WithContext wrapper.
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)
}
}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)Combine backoff policies with WithMaxRetries for circuit breaker patterns:
b := backoff.WithMaxRetries(
backoff.NewExponentialBackOff(),
5, // Maximum 5 attempts
)Wrap non-retryable errors with Permanent():
if isClientError(err) {
return backoff.Permanent(err)
}
return err // Will retryUse 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)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),
)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
}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
}