Library / SDK
sethvargo/go-retry avatar
sethvargo/go-retry

go-retry composes backoff, jitter and limits as middleware

Go library for retrying with configurable backoffs

722 stars44 forksGoApache-2.0

At a glance

What is it?
go-retry is a Go library for retry logic and backoff, built around a small Backoff interface and a middleware chain, with no dependencies outside the standard library and context awareness built in. It suits a service calling a flaky dependency, and it makes the difference between retries and attempts explicit, which is the detail most retry helpers get wrong.
Who is it for?
Adopt go-retry when you need retry behaviour you can reason about, with a documented distinction between retries and attempts, jitter to avoid a thundering herd, and no third party dependencies in your module graph. Do not adopt it if you need a policy engine, a circuit breaker or per error type classification beyond what you wrap yourself.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 18 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A Backoff interface with middleware around it

Retry is a Go library for facilitating retry logic and backoff, and the design goal is stated as being highly extensible with full control over how and when retries occur, with the option to write your own custom backoff functions by implementing the Backoff interface. The inspiration is Go's own HTTP package, and the extension mechanism is middleware. The feature list spells out the four properties that matter. It is extensible via middleware, so you can write custom backoff functions or use a provided filter. It is independent, with no external dependencies besides the Go standard library, meaning it will not bloat your project. It is concurrent, since unless otherwise specified everything is safe for concurrent use. And it is context-aware, using native Go contexts to control cancellation. The last two are the ones that decide whether a retry library is usable in production. A retry loop that cannot be cancelled through a context will outlive the request that started it, and a backoff counter that is not concurrency safe will race as soon as two goroutines share a limiter.

The database example shows how a retryable error is marked

The usage example connects to a database with database/sql, and it demonstrates the one idea you must get right, which is telling the library that a failure is worth retrying:

golang
if err := retry.Fibonacci(ctx, 1*time.Second, func(ctx context.Context) error {
    if err := db.PingContext(ctx); err != nil {
      // This marks the error as retryable
      return retry.RetryableError(err)
    }
    return nil
  }); err != nil {
    log.Fatal(err)
  }

The comment is the API. An error returned plainly is a terminal error and the retry loop stops, while an error wrapped in retry.RetryableError signals that the operation may be attempted again. That distinction matters more than the backoff schedule, because a library that retries every error will hammer a service that is telling you the request is malformed, and a library that retries nothing is useless. The function passed in takes a context and returns an error, and the retry.Fibonacci entry point takes the context, the base delay and that function. The context is threaded into the operation, so a ping uses PingContext and the cancellation of the request context reaches both the sleep and the call.

Three backoff curves, and the one that suits network faults

Three built-in algorithms are provided. Constant is described as very rudimentary, just returning a constant value, and its sequence runs 1s, 1s, 1s, 1s, 1s, 1s. Exponential is arguably the most common backoff, where the next value is double the previous, giving 1s, 2s, 4s, 8s, 16s, 32s, 64s. Fibonacci uses the Fibonacci sequence, where the next value is the sum of the current and the previous, giving 1s, 1s, 2s, 3s, 5s, 8s, 13s. The documentation is explicit about the character of the Fibonacci curve, that retries happen quickly at first but then gradually slow down, which makes it ideal for network-type issues. That property is the reason to pick it over exponential, since a service that is briefly unavailable tends to come back in seconds, and exponential would spend its first two retries waiting 1s and 2s while the service is already healthy again. Each is constructed with the same shape, NewConstant, NewExponential and NewFibonacci, all taking a base duration.

Modifiers are how you make an unbounded curve bounded

The built-in backoff algorithms never terminate and have no caps or limits, and you control their behavior with middleware, with built-in middleware available and custom middleware possible. The jitter modifiers exist to reduce the chances of a thundering herd. There are three forms. WithJitter returns the next value plus or minus a fixed duration, WithJitterPercent returns the next value plus or minus a percentage of the result, and WithFullJitter returns a random value in the range from zero up to the next value. Full jitter is the most aggressive of the three, since it can return a wait of zero, which is the standard way to spread a retry storm. The termination modifiers come in three sizes. WithMaxRetries takes a count, and the documentation is careful to say this is retries, not attempts, where attempts is retries plus 1, so WithMaxRetries(4) stops after four retries when the fifth attempt has failed. WithCappedDuration ensures an individual calculated duration never exceeds a value, so the sleep values become 1s, 1s, 2s, 2s, 2s. And WithMaxDuration gives a best effort limit on the total execution time, capping the whole retry window rather than any single sleep.

Modifier order changes the result, and the docs say so

The notes and caveats section is short and the ordering point is the one to internalise. The ordering of addition of multiple modifiers will make a difference, and the example given is that you should add CappedDuration before WithMaxDuration, otherwise it may early out too early. The reason is not hard to see. WithMaxDuration is a total budget and WithCappedDuration is a per sleep cap, so if the total budget is applied first, a single long sleep can exhaust it before the cap is ever applied, and the cap becomes irrelevant for the first retry. Another example given is that you could add Jitter before or after capping depending on your desired outcome, which is a real choice rather than a detail. Jitter after capping randomises within the cap, while jitter before capping can produce a value above the cap that then gets clamped, which reduces the spread. The second caveat is about randomness itself, and it is the one most likely to matter in review, since randomization uses math/rand/v2, non-cryptographic and automatically seeded, not crypto/rand. That is the right choice for jitter, where you want speed rather than unpredictability, but it should be a deliberate decision rather than a default you inherit.

Benchmarks, module hygiene and the shape of the source

The README includes benchmarks against other popular Go backoff and retry libraries, which you can run yourself via the benchmark folder, with commas and spacing fixed for clarity. The figures given are Benchmark/cenkalti-7 at 13,052,668 and 87.3 ns/op, Benchmark/lestrrat-7 at 902,044 and 1,355 ns/op, and Benchmark/sethvargo-7 at 203,914,245 and 5.73 ns/op. Those are the project's own numbers for its own benchmark, so the useful reading is the relative ordering rather than the absolute figures. The source tree is correspondingly small. At the top level there are backoff.go, backoff_constant.go, backoff_exponential.go and backoff_fibonacci.go with a test file for each, plus retry.go and its test, a go.mod, a LICENSE and the README. The module is github.com/sethvargo/go-retry, requiring go 1.25, with no require block at all, which is the dependency claim in the feature list made concrete. There is one more piece of hygiene worth noting, since the go.mod carries a retract directive for v0.2.0, with the comment that something weird happened with the v0.2.0 tag where the commit in the module registry does not match the commit on GitHub. The release history shows v0.5.0 on 2026-10-05, v0.4.0 on 2026-07-17 and v0.3.0 on 2024-07-30, and the licence is Apache-2.0.

Editorial conclusion

Adopt go-retry when you need retry behaviour you can reason about, with a documented distinction between retries and attempts, jitter to avoid a thundering herd, and no third party dependencies in your module graph. Do not adopt it if you need a policy engine, a circuit breaker or per error type classification beyond what you wrap yourself. Verify first that the modifier order matches your intent, since the documentation warns that the order of addition changes the behaviour, and that your maximum duration accounts for the backoff schedule you chose.

Frequently asked questions

How do I tell go-retry that an error is worth retrying?

Wrap it with retry.RetryableError. An error returned plainly is treated as terminal and the loop stops, while returning retry.RetryableError marks the failure as retryable. The example wraps a failed db.PingContext call that way.

Which backoff algorithms does go-retry provide?

Constant, which returns a constant value, Exponential, where the next value is double the previous, and Fibonacci, where the next value is the sum of the current and the previous. The documentation notes Fibonacci retries happen quickly at first and then slow down, which it describes as ideal for network-type issues.

How do I stop an infinite retry loop in go-retry?

Use WithMaxRetries for a count, WithCappedDuration to cap each individual sleep, and WithMaxDuration for a best effort limit on the total retry time. The documentation notes that the order of adding modifiers matters, and that CappedDuration should be added before WithMaxDuration.

Does go-retry use cryptographic randomness for jitter?

No. The documentation states that randomization uses math/rand/v2, which is non-cryptographic and automatically seeded, not crypto/rand. For jitter that is usually the right trade, but it should be a deliberate choice.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. sethvargo/go-retry on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sethvargo-go-retry.svg)](https://hysenlabs.com/projects/sethvargo-go-retry)

Was this useful?