examples: make hermetic with httptest instead of httpbin.org ExampleRetry hit a live third-party endpoint with an Output directive, so go test depended on the network and could hang up to the 15m default MaxElapsedTime when it was unreachable. Use an in-process httptest server. Also give ExampleRetry_outcomes an Output so it actually runs.
This is a Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java.
Exponential backoff is an algorithm that uses feedback to multiplicatively decrease the rate of some process, in order to gradually find an acceptable rate. The retries exponentially increase and stop increasing when a certain threshold is met.
go get github.com/cenkalti/backoff/v6
Note the /v6 at the end of the import path.
For most cases, wrap the operation you want to retry in Retry:
result, err := backoff.Retry(ctx, func() (string, error) { resp, err := http.Get("https://www.example.com") if err != nil { return "", err // transient: Retry will try again } defer resp.Body.Close() switch { case resp.StatusCode >= 500: return "", fmt.Errorf("server error: %s", resp.Status) // retried case resp.StatusCode >= 400: // client errors won't fix themselves, so stop retrying. return "", backoff.Permanent(fmt.Errorf("client error: %s", resp.Status)) } return "ok", nil }, backoff.WithMaxTries(5))
Retry runs the operation at least once and keeps retrying with exponential backoff until it succeeds, returns a Permanent error, or a limit is reached. See example_test.go for a fuller example, and the package docs for the available options (WithBackOff, WithMaxTries, WithMaxElapsedTime, WithNotify).
If Retry does not fit your needs, copy it from retry.go and adapt it.
On failure, Retry always returns a *RetryError. It carries the last operation error (LastErr) and the reason retrying stopped (Cause). Inspect it with errors.Is, or reach the struct with AsRetryError:
result, err := backoff.Retry(ctx, operation) switch { case errors.Is(err, backoff.ErrPermanent): // the operation returned a Permanent error case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded): // the caller's context was cancelled or its deadline expired case errors.Is(err, backoff.ErrMaxElapsedTime): // the WithMaxElapsedTime budget was exhausted case errors.Is(err, backoff.ErrExhausted): // WithMaxTries was reached or the backoff policy returned Stop } // The last operation error is always available, whatever the cause: if re := backoff.AsRetryError(err); re != nil { log.Printf("gave up after last error: %v", re.LastErr) }
Mark an error non-retriable with backoff.Permanent(err); Retry stops immediately and returns a *RetryError whose Cause is ErrPermanent and whose LastErr is err.
Two independent limits cap how long Retry runs, and they behave differently:
context.WithTimeout) is reactive: it interrupts the wait between attempts and — if your operation observes the context — can abort an in-flight attempt. Retry reports it as context.DeadlineExceeded.WithMaxElapsedTime bounds only retry scheduling: it is checked between attempts, never interrupts a running operation, and is reported as ErrMaxElapsedTime.WithMaxElapsedTime defaults to 15 minutes, so both limits are active unless you override it — pass backoff.WithMaxElapsedTime(0) to rely solely on the context.