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 is the same epoch grid FixedWindowStrategy.{Take,After} use, and it is correct for ANY Size (unlike time.Truncate's year-1 anchoring, which only matches for sizes that divide the year1->epoch offset). 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.