ratelimit

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package ratelimit is an in-memory, fixed-cost request limiter.

Four properties shaped it, and each is a trade rather than an oversight.

It is in-memory and per-instance, not backed by Redis. The surfaces being protected include the redirect path, whose entire budget is 20ms, so spending a network round trip to decide whether to allow a request would cost more than the limit saves. Redis is also optional at runtime by design, and a limiter that stops limiting when the cache goes away is worse than one whose numbers are per-instance. The consequence is stated rather than hidden: with N replicas the effective limit is N times the configured one.

IPv6 is keyed by /64, not by address. A single host is routinely handed a /64 or larger, so a per-address key would let one machine present an effectively unlimited number of identities — defeating the limit and growing the table without bound while doing it.

It fails open. When the key table is full and a sweep cannot free room, the request is allowed and a counter increments. A limiter is abuse mitigation, not an authorization boundary; refusing real traffic because bookkeeping ran out of space would turn a memory ceiling into an outage.

It is a token bucket with lazy refill, so there is no timer per key and no background goroutine. Sweeping is amortized across calls, which means a limiter cannot outlive the thing that created it or leak a goroutine into a test binary.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Key

func Key(addr netip.Addr) string

Key folds an address to its rate-limiting identity: the full address for IPv4, the /64 prefix for IPv6.

The IPv6 case is the one that matters. Handing out /64s to single hosts is normal, so per-address keying would let one machine rotate through more identities than the table could ever hold — the limit would silently stop applying to precisely the client working hardest to evade it.

func RetryAfterSeconds

func RetryAfterSeconds(d time.Duration) int

RetryAfterSeconds renders a wait as an HTTP Retry-After value.

Rounded up, with a floor of 1: Retry-After: 0 invites an immediate retry that is certain to be throttled again, and a client honouring it politely would hammer the endpoint it was just asked to back off from.

Types

type Limiter

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

Limiter allows or throttles requests by client address.

Every method is nil-safe, and a nil Limiter allows everything. That is what makes "0 disables this limit" a single check at construction rather than a branch at every call site.

func New

func New(perMinute int, opts Options) *Limiter

New returns a limiter allowing perMinute requests per key per minute, or nil if perMinute is zero or negative.

Returning nil for a disabled limit is deliberate: the caller stores it, every method tolerates it, and there is no second "enabled" flag to keep in sync with the number.

func (*Limiter) Allow

func (l *Limiter) Allow(addr netip.Addr) (bool, time.Duration)

Allow consumes one token, reporting whether the request may proceed and, if not, how long until it could.

func (*Limiter) Charge

func (l *Limiter) Charge(addr netip.Addr)

Charge consumes a token if one is available, ignoring the answer.

func (*Limiter) Check

func (l *Limiter) Check(addr netip.Addr) (bool, time.Duration)

Check reports whether a token is available without consuming one.

Paired with Charge by callers that only bill some outcomes — the redirect path checks before resolving an alias and charges only for a miss, so a working short link never spends a token.

func (*Limiter) Len

func (l *Limiter) Len() int

Len reports tracked keys. For tests and the metrics collector.

func (*Limiter) Overflows

func (l *Limiter) Overflows() int64

Overflows reports how many requests were allowed because the table was full.

Worth a metric rather than a log line: a nonzero and climbing value means the limiter is no longer limiting, which is exactly the moment an operator wants to know without having to grep.

type Options

type Options struct {
	// Burst is how many requests may arrive at once before throttling starts.
	// Defaults to the per-minute rate, which lets a client spend its whole
	// minute's allowance immediately — right for scripts that batch, and still
	// bounded by the refill rate over any longer window.
	Burst int

	// MaxKeys bounds tracked keys across all shards. Zero means defaultMaxKeys.
	MaxKeys int

	// Now overrides the clock, for tests.
	Now func() time.Time
}

Options tunes a Limiter. The zero value is valid.

Jump to

Keyboard shortcuts

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