Skip to main content
Version: Next (unreleased)

Core Concepts

Every limiter in the package — in memory or Redis-backed — implements the same small set of interfaces. Learn them once and every algorithm works the same way.

The four interfaces​

InterfaceForMethods
Limiterone shared limitAllow, AllowN, Wait, WaitN, Reset
ResultLimiterone shared limit, with detailCheck, CheckN, Take, TakeN
KeyedLimitera limit per keyAllow, AllowN, Wait, WaitN, Reset, ResetAll — each taking a key
KeyedResultLimitera limit per key, with detailCheck, CheckN, Take, TakeN — each taking a key

Every single-limit type (TokenBucket, LeakyBucket, FixedWindow, SlidingWindow, SlidingWindowCounter) implements both Limiter and ResultLimiter. Every keyed type (KeyedTokenBucket, KeyedLeakyBucket, KeyedFixedWindow, KeyedSlidingWindow, and the three Redis limiters) implements both KeyedLimiter and KeyedResultLimiter.

Accept the narrowest interface your code needs. The HTTP middleware, for example, takes a KeyedResultLimiter, so it works with any keyed limiter and with the metrics wrapper.

Allow, Check, Take, Wait​

MethodConsumes?Blocks?Returns
Allow / AllowNif allowednobool
Take / TakeNif allowednoResult
Check / CheckNnevernoResult
Wait / WaitNyesuntil allowed or ctx is doneerror

Allow and Take make the same decision; Take also tells you why. Check answers "would this be allowed right now?" without spending anything — useful for showing a quota in a UI, or for checking several limiters before committing to any of them, which is exactly what Multi does.

The N variants consume n units at once, for operations that cost more than one request — a bulk API call that counts as 50, for example:

if !limiter.AllowN("user:42", 50) {
return errQuotaExceeded
}

On a keyed limiter, Check and CheckN never create state for an unknown key: checking a key nobody has used reports a full allowance and leaves the limiter unchanged.

Result​

Check and Take return a Result:

type Result struct {
Allowed bool // whether the request is allowed
Limit int // maximum requests allowed (burst, capacity, or window limit)
Remaining int // requests left right now
RetryAfter time.Duration // when denied, how long until it would be allowed
ResetAt time.Time // when the current window resets; zero if the algorithm has no window
}

RetryAfter is only set when the request is denied. ResetAt is set by the window-based algorithms (fixed window, sliding window counter, and the sliding window log when it denies); the bucket algorithms refill continuously, so they leave it zero. The HTTP middleware turns these fields into response headers — see Response headers.

Waiting​

Wait and WaitN sleep for the limiter's own estimate of when the request will fit, then try again, until they succeed or the context ends:

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

if err := limiter.Wait(ctx, "user:42"); err != nil {
return err // ctx.Err(), a validation error for n, or a Redis error when failing closed
}

Always pass a context with a deadline when the wait is on a request path. A Wait with context.Background() on a heavily limited key can block for as long as the limit takes to recover.

Requests that can never fit​

A request for more units than the limiter can ever hold — AllowN(100) on a bucket with a burst of 20 — can never succeed, so the limiters do not pretend it might:

  • Allow* returns false.
  • Check* and Take* return a denied Result with Limit set and a zero RetryAfter, because waiting will not help.
  • Wait* returns ErrExceedsLimit immediately instead of blocking forever.

n <= 0 is handled the same way, with ErrInvalidN from Wait*. See Errors.

Reset​

Reset returns a limiter to its starting state — a full bucket, or an empty window. On a keyed limiter, Reset(key) forgets one key and ResetAll() forgets every key. The Redis limiters can only reset all keys when their client implements KeyDeleter; otherwise ResetAll deletes nothing. It returns no error, so the only signal is ErrResetAllUnsupported passed to the WithErrorHandler callback — without one, the failure is silent. See Resetting.

Thread safety​

Every limiter is safe for concurrent use from any number of goroutines. The in-memory limiters guard their state with a mutex; the Redis limiters run each decision as a single atomic Lua script.