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 ¶
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 ¶
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 ¶
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 ¶
Allow consumes one token, reporting whether the request may proceed and, if not, how long until it could.
func (*Limiter) Check ¶
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.
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.