Configuration
relay is configured entirely through functional options passed
to relay.New. Every option has a sensible default, so an
argument-free client is valid.
client := relay.New(
relay.WithTimeout(2*time.Second),
relay.WithRetry(relay.RetryPolicy{MaxAttempts: 4, RetryOn: relay.RetryIdempotent}),
relay.WithCircuitBreaker(relay.BreakerConfig{FailureThreshold: 5}),
relay.WithBackoff(relay.ExponentialBackoff{Base: 50 * time.Millisecond, Max: 2 * time.Second}),
)
Defaults at a glance
| Option | Default | Effect |
|---|---|---|
WithTimeout | 0 (no per-attempt limit) | Deadline applied to each individual attempt. |
WithRetry | MaxAttempts: 1 | One attempt, i.e. retries disabled. |
WithBackoff | exponential, base 100ms, max 5s | Delay between attempts. |
WithCircuitBreaker | disabled | Trip behaviour on repeated failure. |
WithTransport | http.DefaultTransport clone | Underlying round tripper. |
Timeouts
WithTimeout sets a deadline for a single attempt. relay derives
a child context from the one you pass to each call and applies this duration
to it. The parent context still bounds the total operation, so you get two
independent limits:
- Per-attempt — from
WithTimeout. Caps how long one try may run before relay gives up on it and (if retries remain) starts the next. - Total — from the caller's context. Caps the whole operation across all attempts and backoff sleeps.
Retries
Retries are described by a RetryPolicy:
type RetryPolicy struct {
// MaxAttempts is the total number of tries, including the first.
// A value of 1 disables retries. Values below 1 are treated as 1.
MaxAttempts int
// RetryOn decides whether a result is retryable. If nil, relay uses
// RetryIdempotent.
RetryOn func(resp *http.Response, err error) bool
}
Two ready-made classifiers ship with relay:
| Classifier | Retries on |
|---|---|
relay.RetryIdempotent | Transport errors and 502/503/504 responses, but only for idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS). |
relay.RetryNever | Nothing. Equivalent to leaving retries off. |
Write your own when you need finer control — for example, to honour a
Retry-After header or to retry a specific application error
code:
policy := relay.RetryPolicy{
MaxAttempts: 5,
RetryOn: func(resp *http.Response, err error) bool {
if err != nil {
return true // network/transport failure
}
return resp.StatusCode == 429 || resp.StatusCode >= 500
},
}
Request.GetBody. Bodies created by http.NewRequest
from a bytes.Reader, bytes.Buffer, or
strings.Reader set this automatically. For arbitrary streams,
set req.GetBody yourself or relay will not retry that request.
Backoff
Between attempts relay waits according to the configured
Backoff. The default is exponential with full jitter, which
spreads retries out and avoids synchronised thundering herds:
relay.WithBackoff(relay.ExponentialBackoff{
Base: 100 * time.Millisecond, // delay before the 2nd attempt
Max: 5 * time.Second, // ceiling for any single wait
Jitter: relay.FullJitter, // randomise within [0, computed]
})
The computed delay for attempt n (1-indexed retries) is
min(Max, Base × 2^(n-1)), then reduced by the jitter strategy.
Provide your own Backoff implementation for fixed or
decorrelated schemes; see the API reference.
Circuit breaker
The breaker tracks failures per client and short-circuits calls once an upstream looks unhealthy. It has three states — closed (normal), open (failing fast), and half-open (probing recovery).
type BreakerConfig struct {
// FailureThreshold is the number of consecutive failures that trips
// the breaker from closed to open. Zero disables the breaker.
FailureThreshold int
// SuccessThreshold is the number of consecutive successes in the
// half-open state required to close the breaker again. Defaults to 1.
SuccessThreshold int
// OpenTimeout is how long the breaker stays open before allowing a
// single half-open probe. Defaults to 30s.
OpenTimeout time.Duration
}
While open, calls return relay.ErrCircuitOpen without any
network activity. After OpenTimeout elapses, the next call is
allowed through as a probe; success moves toward closed, failure resets the
open timer.
Custom transport
relay wraps a standard http.RoundTripper. Supply your own to
control connection pooling, TLS, or proxies:
tr := &http.Transport{
MaxIdleConnsPerHost: 32,
IdleConnTimeout: 90 * time.Second,
}
client := relay.New(relay.WithTransport(tr))
relay never mutates the transport you pass; it only calls
RoundTrip on it.
Continue to the API reference for full signatures, or see worked Examples.