Documentation
¶
Overview ¶
Package ratelimit provides a sliding window rate limiter with pluggable storage backends.
The sliding window algorithm blends request counts from the previous and current fixed windows, producing a smooth rate estimate that avoids the boundary burst problem of simple fixed-window counters.
Basic Usage ¶
Create a counter, build a limiter, and call Allow on each request:
counter := ratelimit.NewMemoryCounter(ratelimit.MemoryConfig{})
defer counter.Close()
lim, err := ratelimit.New(counter, 100, time.Minute)
if err != nil {
log.Fatal(err)
}
info, err := lim.Allow(ctx, "user:123")
if err != nil {
// handle error
}
if !info.IsAllowed() {
// reject request, retry after info.RetryAfter
}
Counter Backends ¶
Two counter implementations are provided:
NewMemoryCounter creates an in-memory counter suitable for single-process deployments. It runs a background goroutine to clean up expired window data:
counter := ratelimit.NewMemoryCounter(ratelimit.MemoryConfig{
CleanupInterval: 30 * time.Second,
})
defer counter.Close()
NewRedisCounter creates a Redis-backed counter for distributed deployments. On Redis failure, it automatically falls back to an in-memory counter:
client := redis.MustOpen(ctx, redisConfig)
counter := ratelimit.NewRedisCounter(client, ratelimit.RedisConfig{
Prefix: "api",
})
Key Extractors ¶
KeyFunc extracts a rate-limit key from an HTTP request. Several built-in extractors are provided:
- KeyByIP — client IP address (supports CDN headers)
- KeyByFingerprint — device fingerprint
- KeyByPath — request URL path
- KeyByHeader — arbitrary request header value
Use KeyComposite to combine multiple extractors into a single key:
keyFn := ratelimit.KeyComposite(ratelimit.KeyByIP, ratelimit.KeyByPath) key := keyFn(r) // "192.168.1.1:/api/users"
Peeking ¶
Use Limiter.Peek to check the current rate limit status without incrementing the counter:
info, err := lim.Peek(ctx, "user:123")
Error Handling ¶
The package defines sentinel errors:
- ErrRateLimited — request exceeds the rate limit
- ErrInvalidLimit — limit is zero or negative
- ErrInvalidWindow — window duration is zero or negative
- ErrNilCounter — nil counter provided to New
Use errors.Is to check:
_, err := ratelimit.New(nil, 100, time.Minute)
if errors.Is(err, ratelimit.ErrNilCounter) {
// handle missing counter
}
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrRateLimited is returned when a request exceeds the rate limit. ErrRateLimited = errors.New("ratelimit: rate limit exceeded") // ErrInvalidLimit is returned when the rate limit is zero or negative. ErrInvalidLimit = errors.New("ratelimit: limit must be positive") // ErrInvalidWindow is returned when the window duration is zero or negative. ErrInvalidWindow = errors.New("ratelimit: window must be positive") // ErrNilCounter is returned when a nil Counter is provided. ErrNilCounter = errors.New("ratelimit: counter must not be nil") )
Sentinel errors for rate limit operations.
Functions ¶
func KeyByFingerprint ¶
KeyByFingerprint extracts the device fingerprint as the rate-limit key. Uses pkg/fingerprint.Cookie which excludes IP for stability across networks.
Types ¶
type Counter ¶
type Counter interface {
// Increment atomically adds n to the count for the given key and window.
// The ttl specifies how long the window data should be retained.
// Returns the new count after incrementing.
Increment(ctx context.Context, key string, window time.Time, ttl time.Duration, n int64) (int64, error)
// Get returns the current count for the given key and window.
// Returns 0 if the window has no data or has expired.
Get(ctx context.Context, key string, window time.Time) (int64, error)
// Close releases resources (stops background goroutines, etc.).
Close() error
}
Counter is a pluggable storage backend for window-based request counts. Implementations must be safe for concurrent use.
type Info ¶
type Info struct {
// ResetAt is when the current window ends and counters reset.
ResetAt time.Time
// Limit is the maximum number of requests allowed per window.
Limit int64
// Remaining is the number of requests remaining in the current window.
// Always >= 0.
Remaining int64
// RetryAfter is the duration a rate-limited client should wait before retrying.
// Zero when the request is not rate-limited.
RetryAfter time.Duration
}
Info contains rate limit status for a given key.
type KeyFunc ¶
KeyFunc extracts a rate-limit key from an HTTP request.
func KeyByHeader ¶
KeyByHeader returns a KeyFunc that extracts the named header value.
func KeyComposite ¶
KeyComposite combines multiple key extractors into a single key by joining their non-empty results with ":".
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter enforces rate limits using a sliding window algorithm.
The sliding window blends request counts from the previous and current fixed windows to produce a smooth rate estimate that avoids boundary burst problems.
func New ¶
New creates a Limiter with the given counter, limit, and window size.
The limit must be positive. The window must be positive. Returns an error if any argument is invalid.
func (*Limiter) Allow ¶
Allow reports whether a single request for the given key is allowed. It increments the counter and returns the current rate limit info.
func (*Limiter) AllowN ¶
AllowN reports whether n requests for the given key are allowed. It atomically increments the counter by n and returns the current rate limit info.
The counter is incremented before checking the limit (increment-first strategy). This simplifies atomic operations and prevents clients from gaming window boundaries.
type MemoryConfig ¶
type MemoryConfig struct {
// CleanupInterval controls how often expired windows are removed.
// Zero uses the default (1 minute). Negative disables cleanup.
CleanupInterval time.Duration `env:"CLEANUP_INTERVAL" envDefault:"1m"`
}
MemoryConfig configures the in-memory counter.
type MemoryCounter ¶
type MemoryCounter struct {
// contains filtered or unexported fields
}
MemoryCounter is an in-memory Counter implementation with background cleanup of expired windows. Suitable for single-process deployments.
Example:
counter := ratelimit.NewMemoryCounter(ratelimit.MemoryConfig{
CleanupInterval: 30 * time.Second,
})
defer counter.Close()
func NewMemoryCounter ¶
func NewMemoryCounter(cfg MemoryConfig) *MemoryCounter
NewMemoryCounter creates a new in-memory counter.
The cleanup goroutine runs at the configured interval to remove expired window data. Set CleanupInterval to a negative value to disable cleanup.
func (*MemoryCounter) Close ¶
func (m *MemoryCounter) Close() error
Close stops the background janitor goroutine and marks the counter as closed. Close is idempotent.
type RedisConfig ¶
type RedisConfig struct {
Prefix string `env:"PREFIX"`
}
RedisConfig configures the Redis counter.
type RedisCounter ¶
type RedisCounter struct {
// contains filtered or unexported fields
}
RedisCounter is a Redis-backed Counter implementation with automatic fallback to an in-memory counter on connection failure.
Example:
client := redis.MustOpen(ctx, redisConfig)
counter := ratelimit.NewRedisCounter(client, ratelimit.RedisConfig{
Prefix: "api",
})
func NewRedisCounter ¶
func NewRedisCounter(client redis.UniversalClient, cfg RedisConfig) *RedisCounter
NewRedisCounter creates a new Redis-backed counter. The client should be obtained from pkg/redis.Open or pkg/redis.MustOpen.
func (*RedisCounter) Close ¶
func (r *RedisCounter) Close() error
Close releases fallback resources if initialized. The Redis client lifecycle is managed separately by the caller.