avast/retry-go v5: a Go retry library built around a reusable retrier
Simple golang library for retry mechanism
At a glance
- What is it?
- retry-go wraps a failing function in a retry loop with configurable attempts and delays. Version 5.0.0 replaced the package-level API with a method-based Retrier, and that is the change most existing code has to absorb.
- Who is it for?
- Adopt retry-go if you wrap network calls, database pings or any operation that fails transiently and you want the loop, the delay policy and the error history in one small dependency. Do not adopt it if you need a scheduler, a circuit breaker or distributed coordination: the library retries a function in the current process and nothing more.
- Can I use it commercially?
- Yes. MIT 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 40 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 September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What retry-go actually removes from your code
Every Go service that talks to another service eventually grows the same loop: call the function, check the error, sleep, try again, give up after N attempts, and remember what went wrong. retry-go packages that loop. The README describes it as a "Simple library for retry mechanism", and the repository layout backs that up: options.go and retry.go at the top level, plus an examples directory and a test file named last_error_test.go.
The target user is a Go developer who has already decided that a specific operation should be retried and wants to express the policy without writing the control flow. The library does not decide what is retryable. It does not classify HTTP status codes, and it does not know that a 502 differs from a 404. The caller's function returns an error or nil, and the library decides only whether to call it again. That boundary is the whole design, and it is why the package stays small.
One thing the README is explicit about: the project was "Slightly inspired by Try::Tiny::Retry", a Perl module. The Go version keeps the same shape, a wrapped callable plus options, and adds Go-specific concerns such as context cancellation and error unwrapping.
The Retrier: how v5 changed the data flow
Before v5.0.0 the entry point was a package-level function. The migration note in the README states it plainly: retry.Do(func, config) became retry.New(opts...).Do(func). The Config type was renamed to Retrier, NewConfig() became New(), and DelayTypeFunc changed signature from func(n uint, err error, config *Config) to func(n uint, err error, r *Retrier).
The practical consequence is that configuration and execution are now separate objects. retry.New takes the options once and returns a value you can hold. Each call to its Do method runs one retry sequence. The README frames this under "Reusable retrier for high-frequency retry operations", noting "Create retrier once, reuse many times" and "Minimal allocations in happy path".
Inside a single Do call the flow is what you would expect. The library invokes your function. On error it consults the delay type to compute how long to wait, sleeps, and invokes again, up to the configured attempt count. When the sequence ends, you get either nil or an error that aggregates what happened. The README documents an Error type as "list of errors", and one of the example files is examples/errors_history_test.go, which is where that history is exercised.
Delay policy is pluggable through DelayTypeFunc, which the README says "is called to return the next delay to wait after the retriable function fails on err after n attempts". The package ships BackOffDelay, FixedDelay, RandomDelay and FullJitterBackoffDelay. The last one is documented with an explicit formula: sleep = random_between(0, min(cap, base * 2^attempt)), using config.Delay as the base and config.MaxDelay as the cap. CombineDelay merges several delay types into one.
Installing retry-go and running a first retry
The module path in go.mod is github.com/avast/retry-go/v5, so the import carries the v5 suffix. Add it with go get and then import it:
go get github.com/avast/retry-go/v5The README's HTTP example shows the core call. Build a Retrier with an attempt count and a delay, then pass it a function that returns an error:
retrier := retry.New(
retry.Attempts(5),
retry.Delay(100*time.Millisecond),
)
err := retrier.Do(func() error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body, err = ioutil.ReadAll(resp.Body)
return err
})If every attempt fails, err is non-nil and carries the accumulated failures. If any attempt succeeds, err is nil and body holds the response. Note that the example uses ioutil.ReadAll, which is what the README shows; in current Go you would normally write io.ReadAll instead.
When the retried function needs to hand a value back, the README offers DoWithData. The function signature becomes func() ([]byte, error), and the call returns the value and the error together:
body, err := retry.DoWithData(retry.New(),
func() ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
},
)The README points to the examples directory for more, and lists four files there: custom_retry_function_test.go, delay_based_on_error_test.go, errors_history_test.go and http_get_test.go.
Unrecoverable errors and the errors.Unwrap trap
Not every failure deserves another attempt. retry-go exposes Unrecoverable(err error) error, which the README says "wraps an error in unrecoverableError struct", and IsRecoverable(err error) bool, which "checks if error is an instance of unrecoverableError". Return an unrecoverable error from your function and the library can stop rather than burn the remaining attempts on a request that will never succeed.
That is the mechanism to reach for when you can distinguish a permanent failure from a transient one. The library gives you the wrapper, not the classification. Deciding that a 401 is permanent while a 503 is transient remains your code's job, and the README does not offer a status-code helper.
The v5.0.0 notes flag a subtler change. Unwrap() now returns []error instead of error, to support Go 1.20 multiple error wrapping. The note adds that errors.Unwrap(err) will now return nil, "same as errors.Join", and directs callers to errors.Is or errors.As instead. Any code that walked the chain by hand with errors.Unwrap will silently stop finding the underlying error after the upgrade. That is the kind of breakage a compiler will not catch, so it is worth grepping for before you move.
Attempts(0) is documented as meaning infinite retry, a behaviour introduced in 4.0.0. An infinite retry loop with no context cancellation is a way to hang a request handler indefinitely, so the option deserves care.
Where retry-go is the wrong tool
The library retries in the calling goroutine. There is no queue, no worker pool and no persistence, so a retry sequence dies with the process. If your requirement is that a failed job survives a restart, retry-go cannot express it and you need a durable queue in front of the work.
There is also no circuit breaker. If a downstream service is down, every caller retrying five times with backoff multiplies the load on a service that is already failing. retry-go will do exactly what you configure, including making an outage worse. Backoff with jitter (FullJitterBackoffDelay) reduces the thundering-herd problem but does not stop it. A retry library is not a substitute for a breaker, and this one does not claim to be.
The README's own SEE ALSO section is candid about the alternatives, and it is worth reading as a map of the space rather than a sales pitch. sethgrid/pester is described as "only http retry for http calls with retries and backoff". cenkalti/backoff is described as a "Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java" with a "Really complicated interface". The README also links codeGROOVE-dev/retry, calling it a "Modern fork of avast/retry-go/v4" that is "100% API-compatible drop-in replacement".
That last entry is the honest comparison point. If you want the retry-go call style but a different maintenance line, the fork is the direct option, and the README presents it as API-compatible with v4 rather than v5. If your retries are all HTTP, pester targets that case specifically. If you want the Google backoff algorithm and are willing to accept a heavier interface, cenkalti/backoff is the reference implementation. The difference between these and retry-go is mostly surface area: retry-go's selling point is that the wrapper is one function and the options are a variadic list.
Maintenance, licence and the cost of the v5 upgrade
The repository is not archived, and the last push was on 2026-08-21. Releases are tagged: v5.0.0 on 2025-11-24, v4.7.0 on 2025-10-14, and v4.6.1 on 2025-02-24. The 4.x line and the 5.x line both saw activity within roughly a year of each other, which matters if you are pinned to v4 and wondering whether fixes will land there.
The licence is MIT, per the LICENSE file and the badge in the README. That is permissive: you can use the library in closed-source products, and the obligation is essentially to keep the copyright notice and licence text with the distribution. This is not legal advice, and if your organisation has a policy on third-party dependencies, the MIT text is short enough to read in full.
The upgrade cost from v4 to v5 is a mechanical find and replace, which the README states directly: retry.Do(func, opts...) becomes retry.New(opts...).Do(func). The complications are the ones a find and replace does not fix. A custom DelayTypeFunc must change its third parameter from *Config to *Retrier. Code that inspects wrapped errors with errors.Unwrap must move to errors.Is or errors.As. Any local helper that took a *retry.Config now takes *retry.Retrier.
The dependency footprint is small. go.mod requires github.com/stretchr/testify v1.11.1 for tests, with go-spew, go-difflib and yaml.v3 as indirect test dependencies. The module targets go 1.20. None of that ships into your binary beyond the library itself. The Makefile shows the project's own workflow, including a lint target that runs golangci-lint 2.5.0 in Docker and a generate target that regenerates README.md with godocdown, so the README is generated from the Go doc comments rather than maintained by hand.
Editorial conclusion
Adopt retry-go if you wrap network calls, database pings or any operation that fails transiently and you want the loop, the delay policy and the error history in one small dependency. Do not adopt it if you need a scheduler, a circuit breaker or distributed coordination: the library retries a function in the current process and nothing more. Before upgrading, verify your call sites against the v5.0.0 migration note, since retry.Do(func, opts...) is no longer the entry point, and check whether any code still calls errors.Unwrap on the returned error, which now yields nil.
Frequently asked questions
How do I install avast/retry-go?
Run go get github.com/avast/retry-go/v5. The module path in go.mod carries the v5 suffix, so the import statement must include it.
What is the difference between avast/retry-go v4 and v5?
v5.0.0 is a complete API redesign: configuration and execution were split, Config was renamed to Retrier, NewConfig() became New(), and the entry point changed from retry.Do(func, opts...) to retry.New(opts...).Do(func). The README describes the migration as a simple find and replace.
Does avast/retry-go support exponential backoff with jitter?
Yes. FullJitterBackoffDelay calculates the delay as a random value between 0 and the current backoff ceiling, using config.Delay as the base and config.MaxDelay as the cap. BackOffDelay, FixedDelay and RandomDelay are also provided, and CombineDelay merges several delay types.
How do I stop avast/retry-go from retrying a permanent error?
Wrap the error with retry.Unrecoverable before returning it from the retried function. IsRecoverable checks whether an error is an instance of unrecoverableError. The library provides the wrapper, but classifying which failures are permanent remains your code's responsibility.
Official sources
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.
[](https://hysenlabs.com/projects/avast-retry-go)