go 126 lines · 3 tabs

Exponential Backoff With Full Jitter for Flaky HTTP Calls in Go

Shared by codesnips Jul 2026
3 tabs
package httpretry

import (
	"math/rand"
	"time"
)

type Policy struct {
	Base        time.Duration
	Max         time.Duration
	MaxAttempts int
}

func DefaultPolicy() Policy {
	return Policy{
		Base:        100 * time.Millisecond,
		Max:         5 * time.Second,
		MaxAttempts: 5,
	}
}

// Delay returns a full-jitter backoff duration for the given attempt (0-based).
func (p Policy) Delay(attempt int) time.Duration {
	ceiling := p.Base << uint(attempt)
	if ceiling <= 0 || ceiling > p.Max {
		ceiling = p.Max
	}
	return time.Duration(rand.Int63n(int64(ceiling)))
}
3 files · go Explain with highlit

This snippet shows a small, self-contained retry layer for HTTP requests in Go, built around the idea that transient failures (dropped connections, 429s, brief 5xx blips) should be retried with progressively longer, randomized waits rather than a fixed sleep. The core pattern is exponential backoff with full jitter: each attempt waits a random duration in [0, base * 2^attempt], capped at a maximum. Randomizing the delay spreads out retries from many clients so they don't reconverge into a synchronized "thundering herd" that hammers a recovering service in lockstep.

In backoff.go, Policy holds the tunables — Base, Max, and MaxAttempts — and Delay computes the wait for a given attempt. It shifts Base left by the attempt number to get the exponential ceiling, clamps that to Max to avoid runaway waits, then returns rand.Int63n(ceiling) for full jitter. Guarding against overflow via the ceiling <= 0 check matters because shifting a time.Duration by a large attempt count can wrap to a negative value.

retry.go contains the driver. Do loops up to MaxAttempts, invoking the supplied fn and inspecting its error. The retryableError sentinel lets callers signal "this is worth retrying" while other errors abort immediately — a deliberate choice so that permanent failures (a 400, a validation error) don't waste attempts. Between tries it blocks on a select over time.After(delay) and ctx.Done(), so a cancelled or timed-out context aborts the wait promptly instead of sleeping through it. The last error is preserved and returned once attempts are exhausted.

client.go ties it together in FetchJSON. It builds a request bound to the caller's ctx, and inside the retry closure classifies the outcome: network errors and status codes in the retryable set (429, 500, 502, 503, 504) are wrapped as retryableError, while a 4xx like 404 returns a plain error that stops the loop. Note that the response body is drained and closed on the non-2xx path to avoid leaking connections from the pool.

The main trade-off is latency versus success rate: retries can multiply tail latency, so Max and MaxAttempts should be tuned against an overall deadline carried by ctx. This approach fits idempotent GETs and safe writes; non-idempotent operations need idempotency keys before retrying blindly.


Related snips

Share this code

Here's the card — post it anywhere.

Exponential Backoff With Full Jitter for Flaky HTTP Calls in Go — share card
Link copied