hostrate

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 6, 2026 License: MIT Imports: 5 Imported by: 0

README

hostrate

Go Reference Go

A tiny, dependency-light http.RoundTripper that rate-limits outbound HTTP requests per host (or by any key you choose), so your client stays polite to each remote server independently instead of sharing one global budget across all of them.

client := hostrate.NewClient(2, 5) // 2 req/s per host, burst 5
resp, err := client.Get("https://example.com/feed.xml")

Why

Most Go rate-limiting libraries are inbound (limit requests to your server, e.g. go-chi/httprate, sethvargo/go-limiter). For outbound calls, people reach for golang.org/x/time/rate and almost always hand-roll the same map[host]*rate.Limiter + mutex pattern — it's even in the Go wiki. hostrate is that pattern, done once, properly:

  • Per-host buckets — being slow to api.a.com shouldn't throttle api.b.com. Each key gets its own token bucket, created lazily.
  • Composable — it's a plain http.RoundTripper. Wrap any base transport (your tuned http.Transport, an auth transport, a tracing transport) and drop it into any http.Client. Stack it under retry/circuit-breaker middleware such as failsafe-go.
  • Context-aware — waiting honors the request's context, so deadlines and cancellation work while throttled.
  • Bounded memory — optional idle eviction reclaims limiters for hosts you stop talking to, so it's safe even for crawlers that reach unbounded hosts.
  • Small — one file, standard library plus golang.org/x/time/rate.

Install

go get github.com/richardwooding/hostrate

Requires Go 1.26+.

Usage

Convenience client
// 2 requests/second per host, bursting up to 5, over http.DefaultTransport.
client := hostrate.NewClient(2, 5)
Compose over your own transport
import (
    "net/http"
    "time"

    "github.com/richardwooding/hostrate"
    "golang.org/x/time/rate"
)

base := &http.Transport{MaxConnsPerHost: 10 /* your pooling, TLS, proxy, … */}

transport := hostrate.New(base, rate.Limit(2), 5,
    hostrate.WithIdleTimeout(10*time.Minute), // bound memory across many hosts
)
client := &http.Client{Transport: transport}

limit is a rate.Limit:

  • rate.Limit(2) — 2 requests per second
  • rate.Every(500 * time.Millisecond) — one request every 500ms
  • rate.Inf — no limiting
Choosing the key

By default requests are keyed by host (KeyByHost). Swap in a different key with WithKeyFunc:

// Limit per host+port, so :8443 and :9443 get separate budgets.
hostrate.New(base, rate.Limit(2), 5, hostrate.WithKeyFunc(hostrate.KeyByHostPort))

// Or any custom key — e.g. per API token.
hostrate.WithKeyFunc(func(r *http.Request) string {
    return r.Header.Get("X-Api-Key")
})
Memory and eviction

A limiter is created on first use for each key and, by default, kept for the Transport's lifetime — ideal when the set of hosts is small and known. If your key space is effectively unbounded (a crawler, a webhook fan-out), set WithIdleTimeout so limiters unused for that long are evicted:

hostrate.New(base, rate.Limit(2), 5, hostrate.WithIdleTimeout(10*time.Minute))

Eviction is performed opportunistically (no background goroutine, nothing to Close). Transport.Len() reports how many limiters are currently tracked.

What this is not

License

MIT

Documentation

Overview

Package hostrate provides an http.RoundTripper that rate-limits outbound HTTP requests on a per-host basis (or by any custom key), so a client stays polite to each remote server independently rather than sharing one global budget across all hosts.

Each distinct key gets its own golang.org/x/time/rate.Limiter, created lazily on first use. The default key is the request's host, so traffic to api.a.com is limited separately from api.b.com.

Basic use:

client := hostrate.NewClient(2, 5) // 2 req/s, burst 5, per host
resp, err := client.Get("https://example.com/feed.xml")

Composing with a custom base transport (for example, tuned connection pooling) and keying:

t := hostrate.New(myTransport, rate.Limit(2), 5,
	hostrate.WithKeyFunc(hostrate.KeyByHostPort),
	hostrate.WithIdleTimeout(10*time.Minute), // bound memory for unbounded hosts
)
client := &http.Client{Transport: t}

A Transport blocks each request until its key's limiter permits the call or the request's context is done, so request deadlines and cancellation are honored while waiting.

Example

Example shows the convenience constructor: an http.Client that allows at most 2 requests per second to each host, bursting up to 5.

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"

	"github.com/richardwooding/hostrate"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprint(w, "hello")
	}))
	defer srv.Close()

	client := hostrate.NewClient(2, 5)

	resp, err := client.Get(srv.URL)
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}
Output:
hello

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func KeyByHost

func KeyByHost(req *http.Request) string

KeyByHost keys requests by their lowercased host without port. It is the default KeyFunc.

func KeyByHostPort

func KeyByHostPort(req *http.Request) string

KeyByHostPort keys requests by their lowercased host including port, so the same hostname on different ports is limited independently.

func NewClient

func NewClient(rps float64, burst int, opts ...Option) *http.Client

NewClient is a convenience constructor returning an http.Client whose Transport rate-limits to rps requests per second per host (with the given burst), wrapping http.DefaultTransport. For a custom base transport — for example, tuned connection pooling — build a Transport with New instead.

Types

type KeyFunc

type KeyFunc func(req *http.Request) string

KeyFunc derives the rate-limiting key for a request. Requests that map to the same key share a single token-bucket limiter.

type Option

type Option func(*Transport)

Option configures a Transport passed to New.

func WithIdleTimeout

func WithIdleTimeout(d time.Duration) Option

WithIdleTimeout enables eviction of per-key limiters that have not been used for at least d. This bounds memory when the set of keys is effectively unbounded (for example, a crawler reaching arbitrarily many hosts).

The default is zero, which disables eviction and retains one limiter per key for the Transport's lifetime — appropriate when the set of hosts is small and known. A non-positive d disables eviction.

func WithKeyFunc

func WithKeyFunc(fn KeyFunc) Option

WithKeyFunc sets the function used to derive each request's limiter key. The default is KeyByHost. A nil KeyFunc is ignored.

type Transport

type Transport struct {
	// contains filtered or unexported fields
}

Transport is an http.RoundTripper that applies a per-key token-bucket rate limit before delegating to a base RoundTripper. A separate limiter is created lazily for each distinct key (by default, each target host).

The zero value is not usable; construct a Transport with New. A Transport is safe for concurrent use by multiple goroutines.

func New

func New(base http.RoundTripper, limit rate.Limit, burst int, opts ...Option) *Transport

New returns a Transport that limits requests sharing a key to limit requests per second with the given burst, delegating to base. If base is nil, http.DefaultTransport is used.

limit is a golang.org/x/time/rate.Limit: use rate.Limit(n) for n requests per second, rate.Every(d) for one request per d, or rate.Inf to disable limiting. burst is the maximum number of requests allowed to proceed at once before throttling begins.

Example

ExampleNew shows composing the Transport over a custom base transport, with a custom key and idle eviction to bound memory when reaching many hosts.

package main

import (
	"fmt"
	"net/http"
	"time"

	"golang.org/x/time/rate"

	"github.com/richardwooding/hostrate"
)

func main() {
	base := &http.Transport{MaxConnsPerHost: 10}

	transport := hostrate.New(base, rate.Limit(2), 5,
		hostrate.WithKeyFunc(hostrate.KeyByHostPort),
		hostrate.WithIdleTimeout(10*time.Minute),
	)
	client := &http.Client{Transport: transport}

	_ = client // use client for outbound requests
	fmt.Println("configured")
}
Output:
configured

func (*Transport) Len

func (t *Transport) Len() int

Len reports the number of per-key limiters currently tracked. It is primarily useful for observability and tests.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper. It blocks until the limiter for the request's key permits the call or the request's context is done, then delegates to the base RoundTripper. If the context is canceled or its deadline is exceeded while waiting, RoundTrip returns that error and does not call the base RoundTripper.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL