relay.

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

OptionDefaultEffect
WithTimeout0 (no per-attempt limit)Deadline applied to each individual attempt.
WithRetryMaxAttempts: 1One attempt, i.e. retries disabled.
WithBackoffexponential, base 100ms, max 5sDelay between attempts.
WithCircuitBreakerdisabledTrip behaviour on repeated failure.
WithTransporthttp.DefaultTransport cloneUnderlying 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:

Tip: keep the per-attempt timeout well below the total budget. A common rule of thumb is total ≈ attempts × (timeout + max backoff), so the last attempt still has room to finish.

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:

ClassifierRetries on
relay.RetryIdempotentTransport errors and 502/503/504 responses, but only for idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS).
relay.RetryNeverNothing. 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
	},
}
Note: when relay retries, it rewinds the request body using 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.