relay.

relay

A small, dependency-free HTTP client wrapper for Go that adds retries, timeouts, and circuit breaking around the standard library.

Go 1.21+ MIT licensed Zero dependencies v1.2.1

relay wraps *http.Transport and the standard net/http client with the three resilience patterns most services need when calling other services: bounded retries with backoff, per-attempt and total timeouts, and a circuit breaker that stops hammering a failing upstream. It does not introduce a new request type, a new context model, or a custom logging framework — you keep using *http.Request and context.Context.

Why relay

Retries with backoff

Bounded attempts with exponential backoff and full jitter. You decide which responses and errors are retryable.

Two layers of timeout

A per-attempt deadline keeps a single slow call from eating the whole budget, while the parent context bounds the total.

Circuit breaking

A per-client breaker trips after repeated failures and recovers through a half-open probe, shedding load fast.

Standard library only

No third-party dependencies. The wrapped transport stays a plain http.RoundTripper you can swap out.

Install

Requires Go 1.21 or newer.

go get relay.example/relay@latest

Then import it:

import "relay.example/relay"
Note: the import path above is a placeholder for this documentation. Replace it with the module path of your checkout. See Getting started for details.

Quick start

Construct a client once, reuse it everywhere. A relay.Client is safe for concurrent use.

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"time"

	"relay.example/relay"
)

func main() {
	client := relay.New(
		relay.WithTimeout(2*time.Second),
		relay.WithRetry(relay.RetryPolicy{
			MaxAttempts: 4,
			RetryOn:     relay.RetryIdempotent,
		}),
		relay.WithCircuitBreaker(relay.BreakerConfig{
			FailureThreshold: 5,
			OpenTimeout:      10 * time.Second,
		}),
	)

	ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
	defer cancel()

	resp, err := client.Get(ctx, "http://service.internal/health")
	if err != nil {
		log.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Printf("%d: %s\n", resp.StatusCode, body)
}

From here: