ratelimit

package
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 17, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

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:

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:

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

View Source
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

func KeyByFingerprint(r *http.Request) string

KeyByFingerprint extracts the device fingerprint as the rate-limit key. Uses pkg/fingerprint.Cookie which excludes IP for stability across networks.

func KeyByIP

func KeyByIP(r *http.Request) string

KeyByIP extracts the client IP address as the rate-limit key. Uses pkg/clientip which supports CDN headers (CF-Connecting-IP, X-Forwarded-For, etc.).

func KeyByPath

func KeyByPath(r *http.Request) string

KeyByPath extracts the request URL path as the rate-limit key.

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.

func (Info) IsAllowed

func (i Info) IsAllowed() bool

IsAllowed reports whether the request was allowed (not rate-limited).

type KeyFunc

type KeyFunc func(r *http.Request) string

KeyFunc extracts a rate-limit key from an HTTP request.

func KeyByHeader

func KeyByHeader(name string) KeyFunc

KeyByHeader returns a KeyFunc that extracts the named header value.

func KeyComposite

func KeyComposite(funcs ...KeyFunc) KeyFunc

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

func New(counter Counter, limit int64, window time.Duration) (*Limiter, error)

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

func (l *Limiter) Allow(ctx context.Context, key string) (Info, error)

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

func (l *Limiter) AllowN(ctx context.Context, key string, n int64) (Info, error)

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.

func (*Limiter) Peek

func (l *Limiter) Peek(ctx context.Context, key string) (Info, error)

Peek returns the current rate limit info for the given key without incrementing the counter. Useful for checking status in non-request contexts.

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.

func (*MemoryCounter) Get

func (m *MemoryCounter) Get(_ context.Context, key string, window time.Time) (int64, error)

Get returns the current count for the given key and window. Returns 0 if the window has no data or has expired.

func (*MemoryCounter) Increment

func (m *MemoryCounter) Increment(_ context.Context, key string, window time.Time, ttl time.Duration, n int64) (int64, error)

Increment atomically adds n to the count for the given key and window.

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.

func (*RedisCounter) Get

func (r *RedisCounter) Get(ctx context.Context, key string, window time.Time) (int64, error)

Get returns the current count for the given key and window from Redis. Falls back to in-memory counter on Redis failure.

func (*RedisCounter) Increment

func (r *RedisCounter) Increment(ctx context.Context, key string, window time.Time, ttl time.Duration, n int64) (int64, error)

Increment atomically adds n to the count for the given key and window in Redis. Falls back to in-memory counter on Redis failure.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL