Documentation
¶
Overview ¶
Package throttle is a small per-key fixed-window rate limiter. It exists to gate the chassis's unsigned credential-mint endpoints (`/auth/dev/enroll`, `/auth/invitations/consume`) against brute-force probing without dragging in a full rate-limit library.
Sizing rationale: state is one int + one timestamp per active key (typically the caller's IP). Lazy cleanup on every Allow evicts buckets older than 2× the window so the map stays bounded under churn even when long-lived attackers cycle through IPs.
What this is NOT:
- distributed (single process; restart resets counters)
- sliding-window (boundary-burst behaviour is acceptable for our anti-abuse threat model)
- per-user (caller carries no identity at the gate; that's the point of throttling unsigned endpoints)
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Throttle ¶
type Throttle struct {
// contains filtered or unexported fields
}
Throttle is a per-key fixed-window counter. Zero value is not usable — call New.
func New ¶
New returns a Throttle that admits up to `limit` attempts per `window` per key.
limit == 0 (or negative) disables the throttle: every Allow call returns ok=true. This is the cheap kill switch used by tests and by operators setting TXCO_THROTTLE_DISABLED=1.
window must be positive when limit > 0; a zero window would make every attempt count toward the same eternal bucket. The constructor clamps to 1s in that case rather than panicking — defensive against env-misconfiguration without surfacing an error to startup.
func (*Throttle) Allow ¶
Allow records an attempt for key. Returns (true, 0) when the attempt is below threshold (and the count has been bumped); returns (false, retryAfter) when the bucket is exhausted, where retryAfter is the remaining duration in the current window.
Disabled throttles (limit==0) always return (true, 0) without touching state.
Side effect: lazy cleanup of stale buckets. On each call we sweep the map and drop any bucket whose resetAt is more than `window` in the past — that's twice the active-window age, so we never evict a bucket that's still counting. Cleanup is O(n) in map size but n is bounded by recent unique callers, which is small in practice.