# cenkalti/backoff: Exponential Backoff Retry Library for Go

> cenkalti/backoff is a Go port of Google's exponential backoff algorithm that gives the Retry function a structured return type, fine-grained error inspection, and two independent timeout mechanisms, making it a complete retry solution for services and network calls.

**cenkalti/backoff** — ⏱ The exponential backoff algorithm in Go

- Repository: https://github.com/cenkalti/backoff
- Stars: 4,084 · Forks: 227
- Language: Go
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/cenkalti-backoff

## What Problem cenkalti/backoff Solves and Who Uses It

Any service that calls a remote API, a database, or a queue will eventually hit transient failures: network blips, overloaded upstreams, rate-limit responses. The naive response is to retry immediately, which can cause thundering-herd problems when many clients hit a struggling service at the same moment. Exponential backoff addresses this by increasing the wait between attempts multiplicatively. The algorithm comes from networking literature and was codified by Google in their HTTP client library for Java. cenkalti/backoff is a Go port of that algorithm, providing a ready-made Retry function and backoff policy type rather than requiring each team to reimplement the same logic.

The library targets Go services, background workers, and CLI tools that make calls to external systems. It is not specific to HTTP: the operation passed to Retry can be any function that returns a value and an error.

## The Retry Function and Its Structured Error Return

The central function is Retry. It accepts a context, a typed operation function, and zero or more option functions. The operation returns a value and an error:

```go
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:
        return "", backoff.Permanent(fmt.Errorf("client error: %s", resp.Status))
    }
    return "ok", nil
}, backoff.WithMaxTries(5))
```

On failure, Retry always returns a *RetryError. That struct carries two fields: LastErr holds the final error from the operation itself, and Cause holds the reason retrying stopped. The Cause can be inspected with errors.Is:

```go
switch {
case errors.Is(err, backoff.ErrPermanent):
    // the operation returned a Permanent error
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
    // context was cancelled or deadline expired
case errors.Is(err, backoff.ErrMaxElapsedTime):
    // WithMaxElapsedTime budget was exhausted
case errors.Is(err, backoff.ErrExhausted):
    // WithMaxTries was reached
}
```

This structured return means callers do not need to parse error strings to distinguish between a client bug and a timeout.

## Installing cenkalti/backoff and Starting a First Retry

The module path includes the major version suffix. Install with:

```bash
go get github.com/cenkalti/backoff/v7
```

The go.mod requires Go 1.23. Import the package by its full versioned path in your source files. Retry runs the operation at least once and will keep retrying until one of the stopping conditions is met. The README points to example_test.go in the repository for a more complete usage example, and to the package documentation on pkg.go.dev for the full list of available options: WithBackOff, WithMaxTries, WithMaxElapsedTime, and WithNotify.

WithNotify takes a function that receives the error and the next wait duration after each failed attempt, which is the recommended hook for logging retry attempts without adding logging logic inside the operation itself. If the Retry function's calling convention does not match a particular use case, the README explicitly states to copy retry.go from the repository and adapt it.

## Stopping a Retry: Permanent Errors and the WithMaxTries Option

Not all errors are worth retrying. A 400 Bad Request response from an API means the request is malformed and will fail the same way on every attempt. Wrap those errors in backoff.Permanent so Retry stops immediately:

```go
return "", backoff.Permanent(fmt.Errorf("client error: %s", resp.Status))
```

When a Permanent error is returned, Retry stops without waiting, and the RetryError's Cause is ErrPermanent. The original error is still available through LastErr.

WithMaxTries caps the total number of attempts. Reaching the cap sets Cause to ErrExhausted. This option is most useful when the downstream rate limit is expressed in terms of attempt count rather than elapsed time. The two stop mechanisms, WithMaxTries and WithMaxElapsedTime, work independently: whichever limit is reached first stops the retry loop.

## Context Deadline vs. WithMaxElapsedTime: Two Different Timeout Behaviors

The README draws a clear distinction between the two bounding mechanisms. A context deadline passed via context.WithTimeout is reactive: it interrupts the wait period between attempts and, if the operation itself respects the context, can abort an in-flight call. Retry reports a cancelled context as context.Canceled or context.DeadlineExceeded.

WithMaxElapsedTime works differently. It checks the elapsed time only between attempts, after the operation returns. It never interrupts an operation that is already running. If the operation takes longer than the elapsed-time budget, the budget is not exceeded until after that operation returns. Retry reports this as ErrMaxElapsedTime.

The default value for WithMaxElapsedTime is 15 minutes. This default applies even when no option is explicitly passed, so a Retry call with only WithMaxTries(5) is also bounded by the 15-minute elapsed-time limit. To disable the elapsed-time limit entirely and rely solely on the context, pass backoff.WithMaxElapsedTime(0).

## Limitations and Cases Where cenkalti/backoff Is the Wrong Choice

The library is intentionally small. The README states that the author wants to keep the library minimal and will not accept pull requests for features that are not common use cases. Teams who need distributed retry coordination across multiple services, circuit-breaker integration, or retry budgets shared across request pools will not find those features here and should look elsewhere or build on top.

The exponential backoff policy itself does not include jitter by default. Jitter (randomizing the wait interval to spread load across multiple clients) is commonly added on top of exponential backoff in high-concurrency scenarios. The README does not document a built-in jitter option.

The library has no GitHub releases. The version history is tracked through the Go module proxy and the v7 branch. Pinning a dependency to a specific commit rather than a tagged release is less reproducible when the module proxy cache is unavailable.

## Maintenance, License, and Alternatives

The last push to the repository was on 2026-06-30. The library is MIT licensed, which permits use in commercial and open-source projects without requiring derivative works to be open-source. The module requires Go 1.23 or later, as specified in go.mod.

The direct alternative is golang.org/x/net/backoff, which is part of the extended Go standard library but provides a narrower interface. For teams that want retry behavior built into their HTTP client rather than as a separate function, the standard library's net/http package does not include retry logic, so a dedicated library or hand-rolled logic is necessary regardless. The avast/retry-go library is another option for Go that uses a similar function-wrapping approach but differs in its option API and error reporting model.

## Conclusion

cenkalti/backoff fits Go services that need reliable retry logic with clear control over when to stop and why retries ended. The structured RetryError type and the Permanent wrapper make error handling explicit. Teams building large retry orchestration with logging pipelines or custom strategies should check whether they can copy retry.go directly, as the library's author states the project intends to stay small and will not accept contributions that are not common use cases. Verify that your context deadline and WithMaxElapsedTime are set correctly together, since both limits are active by default and the 15-minute default elapsed-time cap applies unless you pass WithMaxElapsedTime(0).

## FAQ

### How do I install cenkalti/backoff in a Go project?

Run go get github.com/cenkalti/backoff/v7 to add the module. Note the /v7 suffix in the import path, which is required for Go modules with a major version above 1. The module requires Go 1.23 or later.

### How do I stop cenkalti/backoff from retrying a specific error?

Wrap the error with backoff.Permanent(err) before returning it from the operation function. Retry will stop immediately when it receives a Permanent error, and the RetryError's Cause will be ErrPermanent.

### What is the default maximum retry duration in cenkalti/backoff?

The default maximum elapsed time is 15 minutes, enforced by WithMaxElapsedTime. This limit applies even when no options are passed explicitly. To remove it and rely only on the context timeout, pass backoff.WithMaxElapsedTime(0) as an option to Retry.

## Sources

- [cenkalti/backoff on GitHub](https://github.com/cenkalti/backoff)
- [Issues](https://github.com/cenkalti/backoff/issues)
- [License: MIT](https://github.com/cenkalti/backoff/blob/v7/LICENSE)
- [README](https://github.com/cenkalti/backoff/blob/v7/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cenkalti-backoff
