relay.

Getting started

Install relay, build a client, and send your first resilient request in a few minutes.

Requirements

relay targets Go 1.21 and newer. It has no third-party dependencies, so a standard toolchain is all you need. Verify your version:

go version

Installation

Add the module to your project:

go get relay.example/relay@latest

Pin a specific release if you prefer reproducible builds:

go get relay.example/relay@v1.2.1
Note: relay.example/relay is a documentation placeholder import path. Use the module path of your own checkout. Nothing in relay contacts the network except the requests you make.

Your first request

The core type is relay.Client. Build one with relay.New and a set of options, then use it like the standard client. Every method takes a context.Context as its first argument.

package main

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

	"relay.example/relay"
)

func main() {
	client := relay.New(
		relay.WithTimeout(time.Second),
		relay.WithRetry(relay.RetryPolicy{MaxAttempts: 3}),
	)

	ctx := context.Background()
	resp, err := client.Get(ctx, "http://example.internal/v1/status")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	b, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(b))
}

How the pieces fit

A single call to Do flows through three layers, outermost first:

  1. Circuit breaker. If the breaker is open, the call returns relay.ErrCircuitOpen immediately without touching the network.
  2. Retry loop. While attempts remain and the parent context is still valid, relay sends the request. Failures classified as retryable trigger a backoff sleep before the next attempt.
  3. Per-attempt timeout. Each individual attempt runs under a derived context bounded by WithTimeout, so one stalled call cannot consume the whole budget.

The parent context you pass always wins: if it is cancelled or its deadline passes, relay stops retrying and returns the context error.

Reuse one client

Create a client once and share it. It holds a connection pool through the underlying transport and a breaker whose state is meaningful only when shared across requests to the same upstream.

// good: one client per upstream, stored on your service struct
type Service struct {
	billing *relay.Client
}

func NewService() *Service {
	return &Service{
		billing: relay.New(relay.WithTimeout(2 * time.Second)),
	}
}

Creating a fresh client per request defeats both connection pooling and the circuit breaker, since each new breaker starts closed with no failure history.

Next steps