Documentation
¶
Index ¶
- func ClientIP(r *http.Request) string
- type ConcurrentQueueStrategy
- type ConcurrentStrategy
- type Event
- type ExceededHandler
- type FixedWindowStrategy
- type LeakyBucketStrategy
- type ObserveFunc
- type RateLimiter
- func Concurrent(capacity int) *RateLimiter
- func ConcurrentQueue(capacity, size int) *RateLimiter
- func FixedWindow(rate int, unit time.Duration) *RateLimiter
- func FixedWindowPerHour(rate int) *RateLimiter
- func FixedWindowPerMinute(rate int) *RateLimiter
- func FixedWindowPerSecond(rate int) *RateLimiter
- func LeakyBucket(perRequest time.Duration, size int) *RateLimiter
- func New(strategy Strategy) *RateLimiter
- func RedisFixedWindow(runner RedisRunner, rate int, unit time.Duration) *RateLimiter
- func RedisFixedWindowPerHour(runner RedisRunner, rate int) *RateLimiter
- func RedisFixedWindowPerMinute(runner RedisRunner, rate int) *RateLimiter
- func RedisFixedWindowPerSecond(runner RedisRunner, rate int) *RateLimiter
- func SlidingWindow(rate int, unit time.Duration) *RateLimiter
- func SlidingWindowPerHour(rate int) *RateLimiter
- func SlidingWindowPerMinute(rate int) *RateLimiter
- func SlidingWindowPerSecond(rate int) *RateLimiter
- type RedisFixedWindowStrategy
- type RedisRunner
- type RedisRunnerFunc
- type Result
- type SlidingWindowStrategy
- type Strategy
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type ConcurrentQueueStrategy ¶
type ConcurrentQueueStrategy struct {
Capacity int // max concurrent at a time
Size int // queue size
// contains filtered or unexported fields
}
ConcurrentQueueStrategy implements Strategy that allow only max concurrent requests at a time other requests will queue, until queue full then the request wil drop
func (*ConcurrentQueueStrategy) After ¶
func (b *ConcurrentQueueStrategy) After(string) time.Duration
After always return 0, since we don't know when request will finish
func (*ConcurrentQueueStrategy) Put ¶
func (b *ConcurrentQueueStrategy) Put(key string)
Put removes requests count
func (*ConcurrentQueueStrategy) Take ¶
func (b *ConcurrentQueueStrategy) Take(key string) bool
Take returns true if current requests less than capacity
type ConcurrentStrategy ¶
type ConcurrentStrategy struct {
Capacity int // Max concurrent at a time
// contains filtered or unexported fields
}
ConcurrentStrategy implements Strategy that allow only max concurrent requests at a time other requests will drop
func (*ConcurrentStrategy) After ¶
func (b *ConcurrentStrategy) After(string) time.Duration
After always return 0, since we don't know when request will finish
func (*ConcurrentStrategy) Put ¶
func (b *ConcurrentStrategy) Put(key string)
Put removes requests count
func (*ConcurrentStrategy) Take ¶
func (b *ConcurrentStrategy) Take(key string) bool
Take returns true if current requests less than capacity
type Event ¶ added in v0.18.0
type Event struct {
// Name is the operator-set RateLimiter.Name (may be ""), so several rate limiters
// can be told apart in metrics. It is bounded by construction (operators set it).
Name string
// Result is allowed or limited.
Result Result
}
Event reports one rate-limit decision to RateLimiter.Observe. It carries only bounded fields: the operator-set limiter Name and the Result. It deliberately does NOT carry the client key, whose cardinality is unbounded and would blow up any per-key metric label set.
type ExceededHandler ¶
ExceededHandler type
type FixedWindowStrategy ¶
type FixedWindowStrategy struct {
Max int // Max token per window
Size time.Duration // Window size
// contains filtered or unexported fields
}
FixedWindowStrategy implements Strategy using fixed window algorithm
func (*FixedWindowStrategy) After ¶
func (b *FixedWindowStrategy) After(key string) time.Duration
After returns next time that can take again
func (*FixedWindowStrategy) Take ¶
func (b *FixedWindowStrategy) Take(key string) bool
Take takes a token from bucket, return true if token available to take
type LeakyBucketStrategy ¶
type LeakyBucketStrategy struct {
PerRequest time.Duration // time per request
Size int // queue size
// contains filtered or unexported fields
}
LeakyBucketStrategy implements Strategy using leaky bucket algorithm
func (*LeakyBucketStrategy) After ¶
func (b *LeakyBucketStrategy) After(key string) time.Duration
After returns time that can take again
func (*LeakyBucketStrategy) Take ¶
func (b *LeakyBucketStrategy) Take(key string) bool
Take waits until token can be take, unless queue full will return false
type ObserveFunc ¶ added in v0.18.0
type ObserveFunc func(Event)
ObserveFunc is the rate-limit observation-hook shape, returned by prom.RateLimit for wiring into RateLimiter.Observe. It fires once per Take decision, IN ADDITION to (never replacing) ExceededHandler, on the request goroutine — keep it cheap.
type RateLimiter ¶
type RateLimiter struct {
Key func(r *http.Request) string
ExceededHandler ExceededHandler
Strategy Strategy
// Observe, if set, is fired on EVERY Take decision with a bounded Event (the Name
// below and allowed/limited). Unlike ExceededHandler it does NOT replace the
// response — observability is independent of what the client sees, so merely
// COUNTING rejections no longer means reimplementing the 429. It runs synchronously
// on the request goroutine; keep it cheap. nil disables it. See prom.RateLimit.
Observe ObserveFunc
// Name is an operator-set, bounded label carried on every Event so several rate
// limiters are distinguishable in metrics; "" is fine (the prom adapter maps it).
// NEVER derived from the client key (unbounded cardinality).
Name string
ReleaseOnWriteHeader bool // release token when write response's header
ReleaseOnHijacked bool // release token when hijacked
}
RateLimiter middleware
Example (ExceededHandler) ¶
Customize the over-limit response with ExceededHandler instead of the default plain-text 429.
package main
import (
"net/http"
"time"
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
m := ratelimit.FixedWindowPerMinute(120)
m.ExceededHandler = func(w http.ResponseWriter, r *http.Request, after time.Duration) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
_, _ = w.Write([]byte(`{"error":"rate limited"}`))
}
s := parapet.New()
s.Use(m)
}
Output:
func Concurrent ¶
func Concurrent(capacity int) *RateLimiter
Concurrent creates new concurrent rate limiter
Example ¶
Concurrent caps the number of in-flight requests per key rather than the arrival rate; once a request finishes, its slot is freed. Excess requests are rejected immediately. Useful for protecting an expensive endpoint from pile-up.
package main
import (
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
s := parapet.New()
s.Use(ratelimit.Concurrent(4)) // at most 4 in-flight requests per client IP
}
Output:
func ConcurrentQueue ¶
func ConcurrentQueue(capacity, size int) *RateLimiter
ConcurrentQueue creates new concurrent queue rate limiter
Example ¶
ConcurrentQueue is like Concurrent but holds excess requests in a bounded queue instead of rejecting them outright: up to Capacity run at once, up to Size wait, anything beyond that is dropped.
package main
import (
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
s := parapet.New()
s.Use(ratelimit.ConcurrentQueue(4, 16)) // 4 concurrent, 16 queued per key
}
Output:
func FixedWindow ¶
func FixedWindow(rate int, unit time.Duration) *RateLimiter
FixedWindow creates new fixed window rate limiter
Example ¶
FixedWindow with an arbitrary window size when the per-second/minute/hour helpers don't fit — here, 100 requests every 5 minutes.
package main
import (
"time"
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
s := parapet.New()
s.Use(ratelimit.FixedWindow(100, 5*time.Minute))
}
Output:
func FixedWindowPerHour ¶
func FixedWindowPerHour(rate int) *RateLimiter
FixedWindowPerHour creates new rate limiter per hour
func FixedWindowPerMinute ¶
func FixedWindowPerMinute(rate int) *RateLimiter
FixedWindowPerMinute creates new rate limiter per minute
func FixedWindowPerSecond ¶
func FixedWindowPerSecond(rate int) *RateLimiter
FixedWindowPerSecond creates new rate limiter per second
Example ¶
Cap each client IP to a fixed number of requests per window. This is the most common setup: callers exceeding the budget get a 429 with a Retry-After header. The default Key is ClientIP, so no further configuration is needed.
package main
import (
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
s := parapet.New()
s.Use(ratelimit.FixedWindowPerSecond(10)) // 10 req/s per client IP
// s.Use(upstream.SingleHost("10.0.0.1:8080")) — the protected backend.
}
Output:
func LeakyBucket ¶
func LeakyBucket(perRequest time.Duration, size int) *RateLimiter
LeakyBucket creates new leaky bucket rate limiter
Example ¶
LeakyBucket smooths bursts by spacing requests out: it admits one request per PerRequest interval, buffering up to Size before dropping. Here, ~5 req/s with a 10-deep buffer.
package main
import (
"time"
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
s := parapet.New()
s.Use(ratelimit.LeakyBucket(200*time.Millisecond, 10))
}
Output:
func New ¶
func New(strategy Strategy) *RateLimiter
New creates new rate limiter
Example ¶
Rate-limit on something other than the client IP by setting Key — here, an API key header, so the limit is per credential regardless of source address.
package main
import (
"net/http"
"time"
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
m := ratelimit.New(&ratelimit.FixedWindowStrategy{
Max: 60,
Size: time.Minute,
})
m.Key = func(r *http.Request) string {
return r.Header.Get("X-API-Key")
}
s := parapet.New()
s.Use(m)
}
Output:
func RedisFixedWindow ¶ added in v0.18.0
func RedisFixedWindow(runner RedisRunner, rate int, unit time.Duration) *RateLimiter
RedisFixedWindow creates a Redis-backed fixed-window rate limiter, so N proxy instances enforce one GLOBAL limit. runner is your injected Redis client adapter. It fails OPEN on a Redis error (availability over strict limiting); build the strategy directly to change that.
func RedisFixedWindowPerHour ¶ added in v0.18.0
func RedisFixedWindowPerHour(runner RedisRunner, rate int) *RateLimiter
RedisFixedWindowPerHour creates a Redis-backed fixed-window limiter per hour.
func RedisFixedWindowPerMinute ¶ added in v0.18.0
func RedisFixedWindowPerMinute(runner RedisRunner, rate int) *RateLimiter
RedisFixedWindowPerMinute creates a Redis-backed fixed-window limiter per minute.
Example ¶
RedisFixedWindow enforces ONE global limit across a fleet of proxy instances by keeping the counter in Redis. Inject your client via a tiny RedisRunnerFunc adapter so pkg/ratelimit depends on no Redis library; it fails open on a Redis error. Here every instance shares a 1000 req/min budget per client IP.
package main
import (
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
// rdb is your *redis.Client (go-redis); the adapter is the only client-specific code.
// runner := ratelimit.RedisRunnerFunc(
// func(ctx context.Context, script string, keys []string, args ...any) (int64, error) {
// return rdb.Eval(ctx, script, keys, args...).Int64()
// })
var runner ratelimit.RedisRunner // = runner above
s := parapet.New()
s.Use(ratelimit.RedisFixedWindowPerMinute(runner, 1000))
}
Output:
func RedisFixedWindowPerSecond ¶ added in v0.18.0
func RedisFixedWindowPerSecond(runner RedisRunner, rate int) *RateLimiter
RedisFixedWindowPerSecond creates a Redis-backed fixed-window limiter per second.
func SlidingWindow ¶ added in v0.18.0
func SlidingWindow(rate int, unit time.Duration) *RateLimiter
SlidingWindow creates new sliding window rate limiter.
func SlidingWindowPerHour ¶ added in v0.18.0
func SlidingWindowPerHour(rate int) *RateLimiter
SlidingWindowPerHour creates new sliding window rate limiter per hour.
func SlidingWindowPerMinute ¶ added in v0.18.0
func SlidingWindowPerMinute(rate int) *RateLimiter
SlidingWindowPerMinute creates new sliding window rate limiter per minute.
func SlidingWindowPerSecond ¶ added in v0.18.0
func SlidingWindowPerSecond(rate int) *RateLimiter
SlidingWindowPerSecond creates new sliding window rate limiter per second.
Example ¶
SlidingWindow smooths the up-to-2x burst a fixed window admits across its boundary, at O(1) memory per key — a time-weighted blend of the current and previous window counts. Same constructors and 429 behavior as FixedWindow.
package main
import (
"github.com/moonrhythm/parapet"
"github.com/moonrhythm/parapet/pkg/ratelimit"
)
func main() {
s := parapet.New()
s.Use(ratelimit.SlidingWindowPerSecond(10)) // ~10 req/s per client IP, no boundary doubling
}
Output:
func (RateLimiter) ServeHandler ¶
func (m RateLimiter) ServeHandler(h http.Handler) http.Handler
ServeHandler implements middleware interface
type RedisFixedWindowStrategy ¶ added in v0.18.0
type RedisFixedWindowStrategy struct {
// Runner is the REQUIRED injected Redis client adapter. A nil Runner makes every
// Take fail per FailOpen (no panic on the hot path).
Runner RedisRunner
// Max is the global tokens per window. Max <= 0 admits nothing.
Max int
// Size is the window size; <= 0 resolves to time.Second. Windows that do not
// divide an hour are fine here (After is epoch-anchored, see below).
Size time.Duration
// Prefix namespaces the Redis keys; "" resolves to "parapet:rl:".
Prefix string
// Timeout bounds each Take's Redis round-trip (a per-call context deadline); <= 0
// resolves to 100ms. It bounds a SLOW (not just failed) Redis off the hot path.
Timeout time.Duration
// FailOpen decides a Redis error: true admits (the constructors' default), false
// denies (the zero value). Fail open trades strict limiting for availability.
FailOpen bool
// OnError observes a Redis error (timeout, dial, script error); nil ignores it.
OnError func(error)
// contains filtered or unexported fields
}
RedisFixedWindowStrategy implements Strategy with a Redis-backed fixed window, so a fleet of proxy instances shares one global counter per key. The hot path is one atomic Lua round-trip; all mutable state lives in Redis (no local map, no mutex).
Config is read once on first use; set fields before serving. The zero value fails CLOSED on a Redis error (deny); the constructors set FailOpen = true (fail open for availability). Like the in-memory FixedWindowStrategy it admits up to 2*Max across a window boundary, ignores Weight, and has no background goroutine.
func (*RedisFixedWindowStrategy) After ¶ added in v0.18.0
func (b *RedisFixedWindowStrategy) After(string) time.Duration
After returns the time until the current window rolls over, with NO round-trip. It is anchored to the SAME integer epoch (now/Size) folded into the Redis key, so the returned duration is exactly the time to the end of the current window's key.
This intentionally DIVERGES from FixedWindowStrategy.After, which uses time.Truncate (anchored to time.Time's year-1 zero) and is therefore only correct for windows that divide an hour; epoch anchoring is correct for ANY Size. The middleware only calls After after a rejecting Take, so it always reflects a window the key is currently in.
func (*RedisFixedWindowStrategy) Put ¶ added in v0.18.0
func (b *RedisFixedWindowStrategy) Put(string)
Put is a no-op: a fixed window does not return tokens (matches FixedWindow).
func (*RedisFixedWindowStrategy) Take ¶ added in v0.18.0
func (b *RedisFixedWindowStrategy) Take(key string) bool
Take atomically increments the current window's Redis counter and reports whether the caller is within Max. Exactly one round-trip; the over-Max request is still counted (the safe direction — never over-admits). On any Redis error (including a Timeout) it fails open or closed per FailOpen.
type RedisRunner ¶ added in v0.18.0
type RedisRunner interface {
Eval(ctx context.Context, script string, keys []string, args ...any) (int64, error)
}
RedisRunner is the minimal Redis surface the distributed limiter needs: run a Lua script and return its integer reply. Inject an adapter over your client (go-redis, rueidis, redigo, …) so pkg/ratelimit hard-depends on no Redis client. keys/args map to the script's KEYS/ARGV. Implementations MUST be safe for concurrent use and MUST return either the script's integer reply or a non-nil error (a broken adapter that returns (0, nil) would be read as "1 request seen" and admit).
type RedisRunnerFunc ¶ added in v0.18.0
type RedisRunnerFunc func(ctx context.Context, script string, keys []string, args ...any) (int64, error)
RedisRunnerFunc adapts a plain func to RedisRunner.
type Result ¶ added in v0.18.0
type Result uint8
Result classifies the outcome of one rate-limit decision, reported via RateLimiter.Observe.
const ( // ResultAllowed: the request was admitted (Strategy.Take returned true). Under a // Redis fail-open this includes requests admitted because Redis was unreachable — // wire RedisFixedWindowStrategy.OnError (prom.RateLimitRedisError) to tell those // apart from genuinely-under-limit admits. ResultAllowed Result = iota // ResultLimited: the request was rejected (Strategy.Take returned false); the // ExceededHandler ran (a 429 by default). ResultLimited )
type SlidingWindowStrategy ¶ added in v0.18.0
type SlidingWindowStrategy struct {
Max int // Max token per window; Max <= 0 admits nothing
Size time.Duration // Window size (the trailing interval the limit applies over)
// contains filtered or unexported fields
}
SlidingWindowStrategy implements Strategy using the sliding-window-counter algorithm: the effective count is a time-weighted blend of the current fixed window's count and the previous window's count, the previous count fading out linearly as the current window elapses. This smooths the up-to-2x burst a plain fixed window admits across its boundary, while keeping O(1) memory per key (two counters) — unlike an exact sliding log, whose per-key memory is attacker-controlled (O(admitted requests), a DoS-amplification footgun at the edge).
It is an APPROXIMATION, not exact: the blend assumes the previous window's requests were uniformly distributed in time. Back-loaded traffic (a burst at the tail of a window) can briefly over-admit and front-loaded traffic can briefly over-throttle, both bounded and typically under ~1% of Max. Use it when you want to remove the fixed window's boundary doubling cheaply; reach for an exact log only if a hard per-window guarantee is worth the unbounded per-key memory.
Per-KEY state is O(1), but the working set (live map entries) is O(distinct keys seen in the last ~2 windows): there is no max-entries cap, so a unique-key flood inflates memory until the background sweep reclaims it — unlike FixedWindow, which clears its whole map each window boundary (a counter cannot, since the previous window's count is load-bearing for the blend). Like the other limiters, window indices ride the wall clock: a non-monotonic step that re-crosses a boundary can shift at most one window's budget (bounded by Max, never FixedWindow's full reset).
func (*SlidingWindowStrategy) After ¶ added in v0.18.0
func (b *SlidingWindowStrategy) After(key string) time.Duration
After returns how long until the next request for key would be admitted. It never mutates shared state: it reads the item under RLock and computes on locals.
It may return 0 even immediately after Take returned false for the same key, if a window boundary falls between the two calls (the read-only roll then shifts curr->prev, freeing budget) — the client genuinely can take now, so do not treat "After > 0 while blocked" as an invariant.
func (*SlidingWindowStrategy) Put ¶ added in v0.18.0
func (b *SlidingWindowStrategy) Put(string)
Put does nothing — this is an arrival-rate limiter, not a concurrency limiter.
func (*SlidingWindowStrategy) Take ¶ added in v0.18.0
func (b *SlidingWindowStrategy) Take(key string) bool
Take admits a request iff the weighted trailing-window count stays within Max, so a steady stream is capped at APPROXIMATELY Max over any trailing window (exactly Max for uniform/front-loaded traffic; see the type doc for the boundary approximation). Max <= 0 admits nothing.