Help us shape the future of Go! Take the 2026 Go Developer Survey

ratelimit

package
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package ratelimit provides HTTP rate limiting middleware with support for per-principal and per-application rate limits.

The package supports multiple storage backends (in-memory, Redis) and integrates with Cedar policies for dynamic tier-based rate limiting.

Index

Constants

View Source
const (
	HeaderRateLimitPolicy = "RateLimit-Policy"
	HeaderRateLimit       = "RateLimit"
	HeaderRetryAfter      = "Retry-After"

	HeaderXRateLimitLimit     = "X-RateLimit-Limit"
	HeaderXRateLimitRemaining = "X-RateLimit-Remaining"
	HeaderXRateLimitReset     = "X-RateLimit-Reset"
)

Response headers. RateLimit-Policy and RateLimit follow the IETF HTTPAPI draft "RateLimit header fields for HTTP"; the X-RateLimit-* headers are kept for clients written against them.

View Source
const (
	RouteToken          = "token"
	RouteLogin          = "login"
	RouteSignup         = "signup"
	RouteBFFAuth        = "bff_auth"
	RouteHealth         = "health"
	RouteStatic         = "static"
	RoutePaymentWebhook = "payment_webhook"
)

Names of the default route policies, for overriding them by name with MergeRoutes.

View Source
const DefaultMaxKeys = 100_000

DefaultMaxKeys is the number of keys MemoryStorage tracks by default.

View Source
const DefaultPolicyName = "default"

DefaultPolicyName names a Limiter's policy in RateLimit-Policy when WithName is not given.

Variables

View Source
var (
	// DefaultAddressLimit is the default layer's limit per client address:
	// generous, so that only floods reach it.
	DefaultAddressLimit = PerMinute(300)

	// DefaultSignedInLimit is the limit per signed-in principal or service
	// account, checked after authentication.
	DefaultSignedInLimit = PerMinute(1200)
)

Default limits of the layers.

View Source
var (
	ErrRateLimitExceeded = errors.New("rate limit exceeded")
	ErrStorageFailure    = errors.New("rate limit storage failure")
)

Common errors returned by the rate limiter.

View Source
var ErrClosed = errors.New("ratelimit: storage closed")

ErrClosed is returned by a closed storage.

View Source
var ErrTooManyKeys = errors.New("ratelimit: too many tracked keys")

ErrTooManyKeys is returned when a new key arrives while the storage is tracking its maximum number of live keys. Callers should treat it as a refusal: keys are attacker-influenced on pre-authentication endpoints, and admitting them without bound would exhaust memory.

Functions

func Anonymous

func Anonymous(r *http.Request) bool

Anonymous reports whether a request has no signed-in caller in its context, for WithSkip.

func ForwardedClientIP deprecated added in v0.14.0

func ForwardedClientIP(trustedProxies ...string) (func(*http.Request) string, error)

ForwardedClientIP returns a function that finds the client's address behind reverse proxies. When the direct peer is one of the trusted proxy networks, it reads X-Forwarded-For from the right, skipping addresses that are themselves trusted proxies, and returns the first address a trusted proxy did not vouch for. Requests whose peer is not a trusted proxy, or whose header holds nothing usable, are attributed to the peer itself.

Deprecated: build a clientaddr.Resolver, which also reads single-value headers such as CF-Connecting-IP, and pass its ClientIP method, or key limits with AddressKey.

func NewRedisClient

func NewRedisClient(cfg *RedisConfig) redis.UniversalClient

NewRedisClient creates a Redis client from configuration.

func PeekForm

func PeekForm(r *http.Request) url.Values

PeekForm returns the fields of a POST request's form body (application/x-www-form-urlencoded) without consuming it: the body is put back unchanged for the handler. Bodies over 64 KiB, other content types and unparseable bodies give no fields. When the form is already parsed it returns r.PostForm.

func PeerIP added in v0.14.0

func PeerIP(r *http.Request) string

PeerIP returns the IP address of the connection's direct peer, with the port removed and IPv4-mapped IPv6 addresses unmapped. It is the one address a client cannot choose, so it is what limits key on by default. It returns "" when the peer address cannot be parsed. It is clientaddr.PeerIP.

func PolicyApplied

func PolicyApplied(ctx context.Context) bool

PolicyApplied reports whether a Policy has limited the request.

func RequestClientID

func RequestClientID(r *http.Request) string

RequestClientID returns the OAuth client a token request names: the HTTP Basic user name (form-decoded, RFC 6749 §2.3.1) or the client_id field of a form body. A body it reads is put back unchanged for the handler. It returns "" when the request names no client.

func RetryAfterSeconds

func RetryAfterSeconds(d time.Duration) int

RetryAfterSeconds is the Retry-After value for a refused request: the wait rounded up to whole seconds, and at least one, so a client never retries at once.

func WriteHeaders

func WriteHeaders(h http.Header, name string, limit Limit, res Result)

WriteHeaders adds the rate limit headers for one checked limit: an item in RateLimit-Policy (quota q and window w in seconds) and in RateLimit (remaining r and seconds to reset t), and sets X-RateLimit-Limit, -Remaining and -Reset. Several limits checked for one request each add their item.

func WriteTooManyRequests

func WriteTooManyRequests(w http.ResponseWriter, wait time.Duration)

WriteTooManyRequests answers 429 with Retry-After set to wait (rounded up, at least one second).

Types

type CoreAPIResolver

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

CoreAPIResolver adapts coreapi.PolicyStore to the LimitResolver interface. It looks up rate limit policies based on the OAuth client ID from JWT claims.

func NewCoreAPIResolver

func NewCoreAPIResolver(store coreapi.PolicyStore, opts ...CoreAPIResolverOption) *CoreAPIResolver

NewCoreAPIResolver creates a LimitResolver that uses coreapi.PolicyStore.

func (*CoreAPIResolver) GetPolicyForRequest

func (r *CoreAPIResolver) GetPolicyForRequest(ctx context.Context, req *http.Request) *coreapi.RateLimitPolicy

GetPolicyForRequest returns the policy that would be applied to a request. Useful for debugging and displaying policy info in responses.

func (*CoreAPIResolver) InvalidateCache

func (r *CoreAPIResolver) InvalidateCache(clientID string)

InvalidateCache clears the policy cache.

func (*CoreAPIResolver) Resolve

func (r *CoreAPIResolver) Resolve(ctx context.Context, key string, req *http.Request) Limit

Resolve implements LimitResolver by looking up the policy for the OAuth client.

type CoreAPIResolverOption

type CoreAPIResolverOption func(*CoreAPIResolver)

CoreAPIResolverOption configures a CoreAPIResolver.

func WithResolverCache

func WithResolverCache(ttl time.Duration) CoreAPIResolverOption

WithResolverCache enables caching of policy lookups.

type Decision

type Decision int

Decision is the outcome of Check.

const (
	// Allowed: the request is within the limit.
	Allowed Decision = iota
	// Refused: the limit is used up; the response was written.
	Refused
	// Unavailable: the storage failed and the check fails closed; the
	// response was written.
	Unavailable
)

Outcomes of Check.

func Check

func Check(w http.ResponseWriter, r *http.Request, storage Storage, name, key string, limit Limit, failOpen bool, logger *slog.Logger) Decision

Check checks one limit inside a handler, on the storage the layers share, for limits that need the request's outcome or a key only the handler knows (an account, a service account). It writes the rate limit headers, and on refusal the 429 or 503 response; with failOpen a storage failure is logged and allowed. name names the policy in the headers and is the key's namespace.

type KeyFunc

type KeyFunc func(r *http.Request) string

KeyFunc extracts the rate limit key from a request. Common implementations include extracting principal ID or client ID from JWT.

func AddressKey

func AddressKey(res *clientaddr.Resolver) KeyFunc

AddressKey returns a KeyFunc that keys on the client address res finds (see clientaddr). A nil res keys on the direct peer, as IPKey does. An unparseable peer address shares one bucket rather than escaping limits.

func ClientEndpointKey

func ClientEndpointKey() KeyFunc

ClientEndpointKey is a convenience function combining ClientKey with EndpointKey.

func ClientKey

func ClientKey() KeyFunc

ClientKey returns a KeyFunc that extracts the OAuth client ID (azp) from JWT claims. Falls back to IP address if no client ID is present.

func CompositeKey

func CompositeKey() KeyFunc

CompositeKey returns a KeyFunc that combines principal and client IDs. Format: "pid:{principal_id}:client:{client_id}" or fallback to IP.

func EndpointKey

func EndpointKey(inner KeyFunc) KeyFunc

EndpointKey wraps another KeyFunc to add endpoint-specific rate limiting. Format: "{inner_key}:path:{path}:method:{method}"

func ForwardedIPKey deprecated added in v0.14.0

func ForwardedIPKey(trustedProxies ...string) (KeyFunc, error)

ForwardedIPKey is IPKey for a server behind reverse proxies: it takes the client address from X-Forwarded-For when the request arrives from one of the trusted proxy networks (see ForwardedClientIP).

Deprecated: use AddressKey with a clientaddr.Resolver, the client-address rule shared with SystemAuth and the BFF.

func IPKey

func IPKey() KeyFunc

IPKey returns a KeyFunc that keys on the IP address of the direct peer (see PeerIP). Behind a reverse proxy every client would share the proxy's address; use ForwardedIPKey there. Forwarding headers are never believed unless a trusted proxy is configured, because any client can send them.

func PrincipalEndpointKey

func PrincipalEndpointKey() KeyFunc

PrincipalEndpointKey is a convenience function combining PrincipalKey with EndpointKey.

func PrincipalKey

func PrincipalKey() KeyFunc

PrincipalKey returns a KeyFunc that extracts the principal ID from JWT claims. Falls back to IP address if no principal is authenticated.

func SignedInKey

func SignedInKey() KeyFunc

SignedInKey keys on the signed-in caller: the principal (person or service account) requestprincipal or the JWT middleware put in the context. Requests without one get "", so pair it with WithSkip(Anonymous).

type KeyKind

type KeyKind string

KeyKind says what a route limit counts requests by.

const (
	// KeyAddress limits each client address (see clientaddr).
	KeyAddress KeyKind = "address"
	// KeyClient limits each OAuth client, named by HTTP Basic credentials
	// or the client_id form field.
	KeyClient KeyKind = "client"
)

Key kinds of a route limit.

type Limit

type Limit struct {
	// Rate is the number of requests allowed per Period.
	Rate int

	// Period is the time window for the rate limit (e.g., time.Minute).
	Period time.Duration

	// Burst is the maximum number of requests allowed in a single burst.
	// If zero, defaults to Rate.
	Burst int
}

Limit defines rate limit parameters.

func DefaultLimit

func DefaultLimit() Limit

DefaultLimit returns a sensible default rate limit (100 requests per minute).

func PerHour

func PerHour(n int) Limit

PerHour creates a rate limit of n requests per hour.

func PerMinute

func PerMinute(n int) Limit

PerMinute creates a rate limit of n requests per minute.

func PerSecond

func PerSecond(n int) Limit

PerSecond creates a rate limit of n requests per second.

type LimitResolver

type LimitResolver interface {
	// Resolve returns the rate limit for the given key and context.
	// The key is typically derived from the principal ID or client ID.
	Resolve(ctx context.Context, key string, r *http.Request) Limit
}

LimitResolver determines the rate limit for a given request. This allows dynamic rate limiting based on user tier, endpoint, etc.

type Limiter

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

Limiter is the main rate limiter type.

func New

func New(storage Storage, opts ...Option) *Limiter

New creates a new Limiter with the given storage and options.

func NewSignedInLimiter

func NewSignedInLimiter(storage Storage, limit Limit, opts ...Option) *Limiter

NewSignedInLimiter returns the limiter for signed-in callers, to install after authentication: keyed by principal or service account (SignedInKey), skipping anonymous requests, failing open, and named "signed_in". A zero limit uses DefaultSignedInLimit. opts are applied after these.

func (*Limiter) Allow

func (l *Limiter) Allow(r *http.Request) (Result, error)

Allow checks if a request should be allowed.

func (*Limiter) Close

func (l *Limiter) Close() error

Close releases resources held by the limiter.

func (*Limiter) Middleware

func (l *Limiter) Middleware() func(http.Handler) http.Handler

Middleware returns an HTTP middleware that enforces rate limits. Every checked response carries the RateLimit-Policy, RateLimit and X-RateLimit-* headers; a refused one is a 429 with Retry-After of at least one second. A storage failure answers 503 unless WithFailOpen is given, and a full MemoryStorage (ErrTooManyKeys) is always a 429.

type MemoryOption

type MemoryOption func(*MemoryStorage)

MemoryOption configures MemoryStorage.

func WithCleanupInterval

func WithCleanupInterval(d time.Duration) MemoryOption

WithCleanupInterval sets the interval for cleaning up expired windows.

func WithMaxKeys added in v0.14.0

func WithMaxKeys(n int) MemoryOption

WithMaxKeys sets the number of keys the storage tracks at once (default DefaultMaxKeys). Expired windows are discarded to make room first.

type MemoryStorage

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

MemoryStorage is an in-memory rate limit storage using sliding window algorithm. Suitable for single-instance deployments and development/testing.

func NewMemoryStorage

func NewMemoryStorage(opts ...MemoryOption) *MemoryStorage

NewMemoryStorage creates a new in-memory rate limit storage.

func (*MemoryStorage) Allow

func (m *MemoryStorage) Allow(ctx context.Context, key string, limit Limit) (Result, error)

Allow implements Storage.Allow using a sliding window algorithm.

func (*MemoryStorage) Close

func (m *MemoryStorage) Close() error

Close implements Storage.Close.

func (*MemoryStorage) Peek

func (m *MemoryStorage) Peek(ctx context.Context, key string, limit Limit) (Result, error)

Peek implements Peeker: it reports what Allow would, without counting.

func (*MemoryStorage) Reset

func (m *MemoryStorage) Reset(ctx context.Context, key string) error

Reset implements Storage.Reset.

type Option

type Option func(*Limiter)

Option configures a Limiter.

func WithFailOpen added in v0.14.0

func WithFailOpen() Option

WithFailOpen lets requests through when the storage fails. By default the middleware fails closed, answering 503, because a storage outage otherwise switches off the limits that protect authentication endpoints at the moment an attacker may be causing it.

func WithKeyFunc

func WithKeyFunc(keyFunc KeyFunc) Option

WithKeyFunc sets the function to extract rate limit keys from requests.

func WithLimit

func WithLimit(limit Limit) Option

WithLimit sets a static rate limit.

func WithLogger added in v0.14.0

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for storage failures.

func WithName

func WithName(name string) Option

WithName names the limit: the policy name in the RateLimit-Policy and RateLimit headers and in metrics. Default: DefaultPolicyName.

func WithObservability

func WithObservability(obs *observability.Observability) Option

WithObservability sets the observability provider for metrics and tracing.

func WithResolver

func WithResolver(resolver LimitResolver) Option

WithResolver sets a custom limit resolver.

func WithSkip

func WithSkip(skip func(r *http.Request) bool) Option

WithSkip exempts the requests skip reports true for, such as anonymous requests to a limit keyed by the signed-in principal.

type Peeker

type Peeker interface {
	// Peek returns what Allow would return, without counting.
	Peek(ctx context.Context, key string, limit Limit) (Result, error)
}

Peeker is storage that can report a key's state without counting a request. MemoryStorage and RedisStorage implement it.

type Policy

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

Policy is the rate limiting a server applies before authentication: a default limit per client address on every route, and route policies for the credential endpoints. Install its Middleware outermost, before authentication; put a signed-in limit (NewSignedInLimiter) after it.

func NewPolicy

func NewPolicy(cfg PolicyConfig) (*Policy, error)

NewPolicy builds a Policy.

func (*Policy) Address

func (p *Policy) Address() *clientaddr.Resolver

Address returns the client-address resolver.

func (*Policy) Middleware

func (p *Policy) Middleware() func(http.Handler) http.Handler

Middleware enforces the default layer and the route policies.

func (*Policy) Route

func (p *Policy) Route(r *http.Request) (Route, bool)

Route returns the route policy for a request, if one matches. Paths are matched relative to where the policy is mounted: inside a chi router mounted with Mount, the path below the mount point.

func (*Policy) Routes

func (p *Policy) Routes() []Route

Routes returns the route table in effect.

func (*Policy) Storage

func (p *Policy) Storage() Storage

Storage returns the policy's storage, for handler limits that share it.

type PolicyConfig

type PolicyConfig struct {
	// Storage keeps the counters (required): MemoryStorage for one
	// instance, RedisStorage when several share the limits.
	Storage Storage

	// Address finds the client address (see clientaddr). Nil uses the
	// direct peer.
	Address *clientaddr.Resolver

	// Default is the default layer's limit per address. Zero uses
	// DefaultAddressLimit; DisableDefault turns the layer off.
	Default        Limit
	DisableDefault bool

	// DefaultFailClosed answers 503 when the storage fails on the default
	// layer. By default the default layer lets requests through and logs;
	// the route limits of the credential endpoints still refuse.
	DefaultFailClosed bool

	// Routes is the route table. Nil uses DefaultRoutes; an empty non-nil
	// table has no route policies.
	Routes []Route

	// ExemptNetworks are client networks, in CIDR notation or as single
	// addresses, exempt from every limit: back-channel callers such as
	// relying parties' servers.
	ExemptNetworks []string

	// KnownClient reports whether an OAuth client exists. With it, client
	// limits give only registered clients their own bucket, and requests
	// naming others share one, so made-up client IDs cannot grow the
	// storage. Optional.
	KnownClient func(ctx context.Context, clientID string) bool

	// Logger receives storage failures. Default: slog.Default().
	Logger *slog.Logger

	// Observability records metrics. Optional.
	Observability *observability.Observability
}

PolicyConfig configures a Policy.

type RedisConfig

type RedisConfig struct {
	// Addr is the Redis server address (e.g., "localhost:6379").
	Addr string

	// Password is the Redis password (optional).
	Password string

	// DB is the Redis database number (default 0).
	DB int

	// PoolSize is the maximum number of connections (default 10).
	PoolSize int

	// Cluster enables Redis Cluster mode.
	Cluster bool

	// ClusterAddrs are the cluster node addresses (used when Cluster is true).
	ClusterAddrs []string
}

RedisConfig contains configuration for connecting to Redis.

type RedisOption

type RedisOption func(*RedisStorage)

RedisOption configures RedisStorage.

func WithKeyPrefix

func WithKeyPrefix(prefix string) RedisOption

WithKeyPrefix sets a prefix for all rate limit keys in Redis.

type RedisStorage

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

RedisStorage is a Redis-backed rate limit storage using sliding window algorithm. Suitable for distributed deployments where multiple instances share rate limit state.

func NewRedisStorage

func NewRedisStorage(client redis.UniversalClient, opts ...RedisOption) *RedisStorage

NewRedisStorage creates a new Redis-backed rate limit storage. The client can be *redis.Client, *redis.ClusterClient, or any UniversalClient.

func (*RedisStorage) Allow

func (r *RedisStorage) Allow(ctx context.Context, key string, limit Limit) (Result, error)

Allow implements Storage.Allow using a sliding window algorithm with Redis.

func (*RedisStorage) Close

func (r *RedisStorage) Close() error

Close implements Storage.Close. Note: This does NOT close the Redis client since it may be shared.

func (*RedisStorage) Peek

func (r *RedisStorage) Peek(ctx context.Context, key string, limit Limit) (Result, error)

Peek implements Peeker: it reports what Allow would, without counting.

func (*RedisStorage) Reset

func (r *RedisStorage) Reset(ctx context.Context, key string) error

Reset implements Storage.Reset.

type Result

type Result struct {
	// Allowed is true if the request should be permitted.
	Allowed bool

	// Remaining is the number of requests remaining in the current window.
	Remaining int

	// ResetAt is when the rate limit window resets.
	ResetAt time.Time

	// RetryAfter is the duration to wait before retrying (only set if not allowed).
	RetryAfter time.Duration
}

Result contains the outcome of a rate limit check.

func Exhausted

func Exhausted(ctx context.Context, storage Storage, name, key string, limit Limit) (Result, bool, error)

Exhausted reports whether the limit for key is used up, without counting a request when the storage can look without counting (Peeker). Storage that cannot look reports false. It is for limits that count only some outcomes, such as failed authentication: check Exhausted before the work, and Record after a counted outcome.

func Record

func Record(ctx context.Context, storage Storage, name, key string, limit Limit) (Result, error)

Record counts one outcome against the limit for key, as Check does, without answering the request.

type Route

type Route struct {
	// Name names the policy in keys and in the RateLimit-Policy header.
	Name string

	// Paths are matched against the request path: "/oauth/token" matches
	// that path only, "/login*" every path starting with "/login", and
	// "/bff/auth/*" every path under "/bff/auth/" (and "/bff/auth"). When
	// several routes match, the longest pattern wins.
	Paths []string

	// Methods restricts the route to these methods; empty matches all.
	Methods []string

	// Exempt skips the default layer and every route limit, for health
	// checks, static assets, and callers whose addresses are shared, such
	// as a payment provider's webhooks.
	Exempt bool

	// Limits are checked in order after the default layer.
	Limits []RouteLimit

	// FailOpen lets requests through when the storage fails. Routes fail
	// closed by default: the credential endpoints refuse rather than run
	// without limits.
	FailOpen bool
}

Route is a route policy: the paths it covers, and either an exemption from address limits or limits checked after the default layer.

func DefaultRoutes

func DefaultRoutes() []Route

DefaultRoutes is the default route table: the token endpoint limited per address and per client; login, sign-up and the BFF's authentication endpoints limited per address, lower than the default; health checks, static assets and the payment webhook exempt. Change entries by name with MergeRoutes.

func MergeRoutes

func MergeRoutes(base []Route, overrides ...Route) []Route

MergeRoutes returns base with each override replacing the route of the same name, and overrides with new names appended. base is not modified.

type RouteLimit

type RouteLimit struct {
	Key   KeyKind
	Limit Limit
}

RouteLimit is one limit of a route policy and what it counts by.

type StaticResolver

type StaticResolver struct {
	Limit Limit
}

StaticResolver always returns the same limit.

func (*StaticResolver) Resolve

func (s *StaticResolver) Resolve(ctx context.Context, key string, r *http.Request) Limit

Resolve implements LimitResolver.

type Storage

type Storage interface {
	// Allow checks if a request should be allowed and updates the counter.
	// Returns the result of the check and any error.
	Allow(ctx context.Context, key string, limit Limit) (Result, error)

	// Reset clears the rate limit state for a key.
	Reset(ctx context.Context, key string) error

	// Close releases any resources held by the storage.
	Close() error
}

Storage defines the interface for rate limit state storage.

type TieredResolver

type TieredResolver struct {
	Tiers    map[string]Limit
	Default  Limit
	TierFunc func(ctx context.Context, key string, r *http.Request) string
	// contains filtered or unexported fields
}

TieredResolver returns different limits based on tier.

func (*TieredResolver) Resolve

func (t *TieredResolver) Resolve(ctx context.Context, key string, r *http.Request) Limit

Resolve implements LimitResolver.

Jump to

Keyboard shortcuts

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