Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/httpretrier"
Adds retries to an HTTP client you already have.
r, err := httpretrier.New(client, httpretrier.WithAttempts(3))
resp, err := r.Do(req)
It wraps, rather than replaces
Retries are applied per request, around an existing HTTPClient, so a client that is already instrumented, authenticated, or both keeps everything it had.
The alternative shape, a retrying client that you construct instead of yours, forces a choice between retries and whatever the other client was doing. Here httpclient provides tracing, logging and redaction, and this wraps it.
The retry decision sees both the response and the error
RetryIfFn receives *http.Response and error:
func(resp *http.Response, err error) bool
Both are needed. A transport failure gives an error and no response. A 503 gives a response and no error, and is often the more retryable of the two. A policy that only inspects the error misses every server-side failure that completed the round trip.
The default retries only when err != nil, which is the conservative starting point.
Read and write policies are not the same
Three built-in policies:
| Policy | For |
|---|---|
RetryIfForReadRequests | idempotent reads, GET and similar |
RetryIfForWriteRequests | state-changing writes, POST, PUT, PATCH |
RetryIfFnByHTTPMethod | picks one of the above from the method |
The distinction exists because a failed write is genuinely ambiguous. When a POST times out, the request may have been received and processed, with only the response lost. Retrying it can duplicate the effect: a second charge, a second order, a second message.
So the write policy is narrower. It retries the cases where the request demonstrably did not reach the server, and not the cases where it might have.
RetryIfFnByHTTPMethod is the sane default for a general-purpose client, since it applies the right policy without the call site having to choose. For a non-idempotent write, an idempotency key is what makes retrying safe, whatever the policy says; stringkey generates a stable one.
Backoff
The delay schedule comes from backoff, configured through WithAttempts, WithDelay, WithDelayFactor, WithJitter, WithMaxDelay and WithJitterStrategy.
Sharing the schedule with retrier means one retry policy applies consistently to HTTP and non-HTTP work, rather than two implementations drifting apart.
Set WithMaxDelay. With a factor of 2 and no ceiling, attempt counts above a handful reach delays measured in minutes.
Retries multiply load on a struggling service
Worth stating plainly: three attempts per request means a degraded upstream receives three times the traffic at exactly the moment it is least able to handle it. Jitter spreads the arrivals but does not reduce the total.
Keep the attempt count low, keep the budget inside the caller’s deadline, and count retries so the pattern is visible before it becomes an outage.
Dependencies
This package reaches no external module: it uses only the Go standard library, plus backoff.
Links
- Reference documentation
- Source code
- In depth: Between Two Attempts: What an HTTP Retry Loop Must Decide
- The client to wrap: httpclient
- Non-HTTP retries: retrier
- All nurago packages