capacitor

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jun 1, 2026 License: EUPL-1.2 Imports: 12 Imported by: 0

README

Get it on Codeberg

Capacitor

A rate-limiting library for Go, backed by Valkey. Atomic limiting logic runs server-side via Lua scripts, making it safe for distributed deployments. Ships with drop-in net/http middleware.

Features

  • 5 rate-limiting algorithms: leaky bucket, fixed window, token bucket, sliding window counter, sliding window log
  • All algorithms execute atomically in a single Valkey round-trip via Lua scripts
  • Standard func(http.Handler) http.Handler middleware, works with any http.Handler-based router
  • Configurable key extraction (IP, header, custom function)
  • IETF RateLimit header fields on every response
  • Fallback strategy when Valkey is unreachable (fail-open or fail-closed)
  • Per-profile rate limits with configurable request-to-profile mapping
  • Optional structured logging (log/slog) and metrics collection

Installation

go get codeberg.org/matthew/capacitor

Requires Go 1.22+ and a running Valkey (or Redis 7+) instance.

Algorithms

The algorithms and Lua scripts in Capacitor follow the patterns described in the Redis rate limiting tutorial, which covers the tradeoffs between all five approaches in depth.

Constructor Algorithm Best for Valkey data structure Accuracy
NewLeakyBucket Leaky bucket (policing) Strict no-burst, constant drain HASH (level + last_leak) Exact
NewFixedWindow Fixed-window counter Simple, low overhead STRING (INCR + EXPIRE) Approximate
NewTokenBucket Token bucket Controlled bursts with steady average rate HASH (tokens + last_refill) Exact
NewSlidingWindowCounter Sliding-window counter Near-exact accuracy with low memory STRING x2 (weighted avg) Near-exact
NewSlidingTimeLog Sliding-window log True rolling window, exact counting SORTED SET Exact
Choosing an Algorithm

Start with the sliding window counter if you are unsure. It handles most API rate limiting scenarios well: low memory, near-exact accuracy, and no boundary bursts. It is the best default choice for distributed rate limiting.

  • Fixed window counts requests in discrete time windows. Minimal Valkey overhead (one key per window), but susceptible to boundary bursts: a client could send the full limit at the end of one window and the full limit at the start of the next, doubling throughput for a brief period. Good for login throttling and simple API limits where approximate enforcement is acceptable.
  • Sliding window log records the exact timestamp of every request in a sorted set, providing a true rolling window with no boundary bursts. Memory grows linearly with request volume (O(n) entries per client). Best for high-value APIs, payment processing, and any scenario where you need exact counts and can afford the memory cost.
  • Sliding window counter blends two fixed-window counters using a weighted average to approximate a true sliding window. Offers near-exact accuracy with the same low memory footprint as fixed window. The two keys use Redis cluster hash tags so they always land on the same cluster slot.
  • Token bucket allows controlled bursts up to bucket capacity while enforcing a steady average rate over time. Tokens accumulate over time; each request consumes one. Ideal for APIs with naturally bursty traffic patterns (e.g. mobile apps that batch requests on launch).
  • Leaky bucket (policing mode) provides strict no-burst behavior. A counter tracks the fill level and drains at a constant rate; requests that would exceed capacity are rejected immediately. Best when your downstream service cannot handle any spikes at all. Note: this is the policing variant only (immediate allow/deny); shaping mode (delayed queue drain) is not implemented.
Why Lua Scripts?

Every algorithm executes a Lua script via EVAL for atomic read-modify-write. Without atomicity, concurrent requests can read the same state, both pass the limit check, and both write back, bypassing the limit. This is a TOCTOU (time-of-check-time-of-use) race condition that matters most under the high-concurrency conditions where rate limiting is critical.

Alternatives like MULTI/EXEC cannot branch on intermediate results, and WATCH/MULTI/EXEC requires retries on every concurrent write, which is the worst behavior for a rate limiter. Lua scripts always complete on the first attempt in a single round trip with no contention.

See the Redis tutorial on why Lua scripts for a detailed comparison.

Quick Start

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/valkey-io/valkey-go"
	"codeberg.org/matthew/capacitor"
)

func main() {
	client, err := valkey.NewClient(valkey.ClientOption{
		InitAddress: []string{"localhost:6379"},
	})
	if err != nil {
		log.Fatal(err)
	}

	limiter := capacitor.NewLeakyBucket(client, capacitor.LeakyBucketConfig{
		Capacity:  10,
		LeakRate:  1,
		Timeout:   500 * time.Millisecond,
	})
	defer limiter.Close()

	mux := http.NewServeMux()
	mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, world!\n"))
	})

	rl := capacitor.NewMiddleware(limiter)

	log.Println("listening on :8080")
	http.ListenAndServe(":8080", rl(mux))
}
Using Other Algorithms
// Fixed window: 100 requests per minute
fw := capacitor.NewFixedWindow(client, capacitor.FixedWindowConfig{
	Limit:   100,
	Window:  time.Minute,
	Timeout: 50 * time.Millisecond,
})

// Token bucket: burst up to 20, refill 5/sec
tb := capacitor.NewTokenBucket(client, capacitor.TokenBucketConfig{
	Capacity:   20,
	RefillRate: 5,
	Timeout:    50 * time.Millisecond,
})

// Sliding window counter: 100 requests per minute (near-exact)
swc := capacitor.NewSlidingWindowCounter(client, capacitor.SlidingWindowCounterConfig{
	Limit:   100,
	Window:  time.Minute,
	Timeout: 50 * time.Millisecond,
})

// Sliding window log: 100 requests per minute (exact)
swl := capacitor.NewSlidingTimeLog(client, capacitor.SlidingTimeLogConfig{
	Limit:   100,
	Window:  time.Minute,
	Timeout: 50 * time.Millisecond,
})

Each algorithm also has a default config constructor:

cfg := capacitor.NewLeakyBucketDefaultConfig()
cfg.Capacity = 50 // customize specific fields
limiter := capacitor.NewLeakyBucket(client, cfg)

Configuration

Each algorithm has its own config struct (LeakyBucketConfig, TokenBucketConfig, etc.). Use the corresponding NewXxxDefaultConfig() constructor for sensible defaults.

Leaky Bucket / Token Bucket
Field Type Description
Capacity int64 Maximum tokens in the bucket
LeakRate / RefillRate float64 Tokens drained/refilled per second
KeyPrefix string Prefix for Valkey keys
Timeout time.Duration Per-call Valkey timeout
Fixed Window / Sliding Window Counter / Sliding Window Log
Field Type Description
Limit int64 Maximum requests per window
Window time.Duration Window duration
KeyPrefix string Prefix for Valkey keys
Timeout time.Duration Per-call Valkey timeout

All config fields are validated in NewXxx(): zero or negative values panic (programmer errors).

Middleware Options

WithKeyFunc

Controls how the rate-limit key is derived from each request. Defaults to KeyFromRemoteIP.

rl := capacitor.NewMiddleware(limiter,
	capacitor.WithKeyFunc(capacitor.KeyFromHeader("X-API-Key")),
)

Built-in key functions:

Function Key source
KeyFromRemoteIP Client IP from RemoteAddr (default)
KeyFromHeader(name) Value of the given HTTP header

You can provide any func(*http.Request) string. Return an empty string to skip rate limiting for that request.

WithDenyHandler

Replaces the default plain-text 429 response.

rl := capacitor.NewMiddleware(limiter,
	capacitor.WithDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusTooManyRequests)
		w.Write([]byte(`{"error":"rate limited"}`))
	})),
)

Limiter Options

Pass these to any algorithm's NewXxx():

Option Description
WithLogger(logger) Structured logger (*slog.Logger)
WithFallback(strategy) FallbackFailOpen (default) or FallbackFailClosed

Middleware Metrics

Attach metrics collection at the middleware layer:

rl := capacitor.NewMiddleware(limiter,
	capacitor.WithMetrics(myCollector),
)

MetricsCollector receives the request key and the matched profile name. The default limiter uses "" as the profile.

Response Headers

Every response includes standard rate-limit headers:

Header Description
RateLimit-Limit Bucket capacity / window limit
RateLimit-Remaining Tokens / requests remaining
RateLimit-Reset Seconds until a slot opens (denied requests only)
Retry-After Same value as RateLimit-Reset (denied requests only)

Per-Profile Rate Limits

Use WithProfiles and WithClassifier to apply different rate limits based on an arbitrary per-request categorization (plan, tier, user type, etc.):

profiles := capacitor.ProfileConfig{
	"basic":   capacitor.NewLeakyBucket(client, capacitor.LeakyBucketConfig{Capacity: 10, LeakRate: 1, Timeout: 50 * time.Millisecond}),
	"premium": capacitor.NewLeakyBucket(client, capacitor.LeakyBucketConfig{Capacity: 100, LeakRate: 10, Timeout: 50 * time.Millisecond}),
}

rl := capacitor.NewMiddleware(defaultLimiter,
	capacitor.WithProfiles(profiles),
	capacitor.WithClassifier(func(r *http.Request) string {
		return r.Header.Get("X-Plan")
	}),
)
  • Each profile is a capacitor.Capacitor, use any algorithm or config
  • If the classifier returns "" or a name not in ProfileConfig, the default limiter is used
  • Omit WithProfiles entirely for single-global-limit behavior
Mixing Algorithms Per Profile
profiles := capacitor.ProfileConfig{
	"basic":   capacitor.NewFixedWindow(client, capacitor.FixedWindowConfig{Limit: 10, Window: time.Minute, Timeout: 50 * time.Millisecond}),
	"premium": capacitor.NewTokenBucket(client, capacitor.TokenBucketConfig{Capacity: 100, RefillRate: 10, Timeout: 50 * time.Millisecond}),
}

Direct Usage (Without Middleware)

You can call the limiter directly for non-HTTP use cases such as background workers or gRPC interceptors:

result, err := limiter.Attempt(ctx, "user:42")
if err != nil {
	// Valkey unreachable: result contains the fallback decision.
	log.Println("fallback used:", err)
}

if !result.Allowed {
	log.Printf("denied, retry after %s\n", result.RetryAfter)
}

Health Check

if err := limiter.HealthCheck(ctx); err != nil {
	log.Fatal("valkey unreachable:", err)
}

License

EUPL-1.2

Documentation

Overview

Package capacitor provides rate limiting backed by Valkey (Redis-compatible). All algorithm implementations are in this package.

Usage:

lim := capacitor.NewLeakyBucket(client, capacitor.NewLeakyBucketDefaultConfig())
mw := capacitor.NewMiddleware(lim)
http.Handle("/", mw(myHandler))

Index

Constants

View Source
const (
	FallbackFailOpen   = ratelimit.FallbackFailOpen
	FallbackFailClosed = ratelimit.FallbackFailClosed
)

Re-exported constants.

Variables

View Source
var (
	ErrEmptyUID     = ratelimit.ErrEmptyUID
	ErrEvalResponse = ratelimit.ErrEvalResponse
)

Re-exported errors.

View Source
var (
	DefaultOptions = ratelimit.DefaultOptions
	WithLogger     = ratelimit.WithLogger
	WithFallback   = ratelimit.WithFallback
)

Re-exported option constructors.

Functions

func KeyFromRemoteIP

func KeyFromRemoteIP(r *http.Request) string

KeyFromRemoteIP extracts the IP from RemoteAddr, stripping the port.

func NewMiddleware

func NewMiddleware(limiter Capacitor, opts ...MiddlewareOption) func(http.Handler) http.Handler

NewMiddleware returns standard net/http middleware that rate-limits requests using the provided Capacitor.

Types

type Capacitor

type Capacitor interface {
	// Attempt checks whether the request identified by uid is allowed.
	// On Valkey errors it returns a fallback result and the underlying error.
	Attempt(ctx context.Context, uid string) (Result, error)
	// HealthCheck verifies connectivity to the backing store.
	HealthCheck(ctx context.Context) error
	// Close releases resources held by the Capacitor.
	Close()
}

Capacitor checks whether a request is allowed under a rate-limiting policy. Implementations must be safe for concurrent use.

func NewFixedWindow added in v0.4.0

func NewFixedWindow(client valkey.Client, cfg FixedWindowConfig, opts ...Option) Capacitor

NewFixedWindow creates a fixed-window Capacitor backed by the given Valkey client.

func NewLeakyBucket added in v0.4.0

func NewLeakyBucket(client valkey.Client, cfg LeakyBucketConfig, opts ...Option) Capacitor

NewLeakyBucket creates a leaky-bucket Capacitor backed by the given Valkey client.

func NewSlidingTimeLog added in v0.4.0

func NewSlidingTimeLog(client valkey.Client, cfg SlidingTimeLogConfig, opts ...Option) Capacitor

NewSlidingTimeLog creates a sliding-window log Capacitor backed by the given Valkey client.

func NewSlidingWindowCounter added in v0.4.0

func NewSlidingWindowCounter(client valkey.Client, cfg SlidingWindowCounterConfig, opts ...Option) Capacitor

NewSlidingWindowCounter creates a sliding-window counter Capacitor backed by the given Valkey client.

func NewTokenBucket added in v0.4.0

func NewTokenBucket(client valkey.Client, cfg TokenBucketConfig, opts ...Option) Capacitor

NewTokenBucket creates a token-bucket Capacitor backed by the given Valkey client.

type ClassifyFunc added in v0.2.0

type ClassifyFunc func(r *http.Request) string

ClassifyFunc determines the rate-limit profile name for a request. An empty return value uses the default limiter.

type FallbackStrategy

type FallbackStrategy = ratelimit.FallbackStrategy

Re-exported types from internal/ratelimit.

type FixedWindowConfig added in v0.4.0

type FixedWindowConfig struct {
	Limit     int64         // maximum requests per window
	Window    time.Duration // window duration
	KeyPrefix string        // Valkey key prefix
	Timeout   time.Duration // per-operation Valkey timeout
}

FixedWindowConfig defines the parameters for a fixed-window rate limiter.

func NewFixedWindowDefaultConfig added in v0.4.0

func NewFixedWindowDefaultConfig() FixedWindowConfig

NewFixedWindowDefaultConfig returns a FixedWindowConfig with sensible defaults.

type KeyFunc

type KeyFunc func(r *http.Request) string

KeyFunc extracts the rate-limit key from an incoming request.

func KeyFromHeader

func KeyFromHeader(name string) KeyFunc

KeyFromHeader returns a KeyFunc that reads the given header.

type LeakyBucketConfig added in v0.4.0

type LeakyBucketConfig struct {
	Capacity  int64         // maximum number of requests the bucket can hold
	LeakRate  float64       // requests drained per second
	KeyPrefix string        // Valkey key prefix for bucket storage
	Timeout   time.Duration // per-operation Valkey timeout
}

LeakyBucketConfig defines the parameters for a leaky-bucket rate limiter.

func NewLeakyBucketDefaultConfig added in v0.4.0

func NewLeakyBucketDefaultConfig() LeakyBucketConfig

NewLeakyBucketDefaultConfig returns a LeakyBucketConfig with sensible defaults.

type MetricsCollector

type MetricsCollector interface {
	RecordAttempt(key, profile string)
	RecordDenied(key, profile string)
	RecordFallback(key, profile string)
	RecordLatency(d time.Duration, profile string)
}

MetricsCollector receives rate-limiter telemetry. Profile is the name from ProfileConfig; empty string for the default limiter.

type MiddlewareOption

type MiddlewareOption func(*middleware)

MiddlewareOption configures the HTTP middleware.

func WithClassifier added in v0.2.0

func WithClassifier(fn ClassifyFunc) MiddlewareOption

WithClassifier sets the function used to route a request to a named rate-limit profile. See WithProfiles.

func WithDenyHandler

func WithDenyHandler(h http.Handler) MiddlewareOption

WithDenyHandler replaces the default 429 response handler.

func WithKeyFunc

func WithKeyFunc(fn KeyFunc) MiddlewareOption

WithKeyFunc sets the function used to derive the rate-limit key. Defaults to KeyFromRemoteIP.

func WithMetrics

func WithMetrics(mc MetricsCollector) MiddlewareOption

WithMetrics enables telemetry recording via the given collector.

func WithProfiles added in v0.2.0

func WithProfiles(profiles ProfileConfig) MiddlewareOption

WithProfiles configures per-profile limiters. Combine with WithClassifier to route requests to named profiles. Unknown or empty profile names fall back to the default limiter.

type Option

type Option = ratelimit.Option

Re-exported types from internal/ratelimit.

type Options added in v0.2.0

type Options = ratelimit.Options

Re-exported types from internal/ratelimit.

type ProfileConfig added in v0.2.0

type ProfileConfig map[string]Capacitor

ProfileConfig maps profile names to Capacitor instances. Use it with WithProfiles and WithClassifier to route requests to different rate-limiting policies.

type Result

type Result struct {
	Allowed    bool
	Remaining  int64
	Limit      int64
	RetryAfter time.Duration
}

Result holds the outcome of a rate-limit check.

func FallbackResult added in v0.2.0

func FallbackResult(strategy FallbackStrategy, limit int64, retryAfterSecs float64) Result

FallbackResult returns a degraded Result based on the given strategy. Algorithm implementations call this when Valkey is unreachable.

type SlidingTimeLogConfig added in v0.4.0

type SlidingTimeLogConfig struct {
	Limit     int64         // maximum requests per window
	Window    time.Duration // window duration
	KeyPrefix string        // Valkey key prefix
	Timeout   time.Duration // per-operation Valkey timeout
}

SlidingTimeLogConfig defines the parameters for a sliding-window log rate limiter.

func NewSlidingTimeLogDefaultConfig added in v0.4.0

func NewSlidingTimeLogDefaultConfig() SlidingTimeLogConfig

NewSlidingTimeLogDefaultConfig returns a SlidingTimeLogConfig with sensible defaults.

type SlidingWindowCounterConfig added in v0.4.0

type SlidingWindowCounterConfig struct {
	Limit     int64         // maximum requests per window
	Window    time.Duration // window duration
	KeyPrefix string        // Valkey key prefix
	Timeout   time.Duration // per-operation Valkey timeout
}

SlidingWindowCounterConfig defines the parameters for a sliding-window counter rate limiter.

func NewSlidingWindowCounterDefaultConfig added in v0.4.0

func NewSlidingWindowCounterDefaultConfig() SlidingWindowCounterConfig

NewSlidingWindowCounterDefaultConfig returns a SlidingWindowCounterConfig with sensible defaults.

type TokenBucketConfig added in v0.4.0

type TokenBucketConfig struct {
	Capacity   int64         // maximum number of tokens the bucket can hold
	RefillRate float64       // tokens refilled per second
	KeyPrefix  string        // Valkey key prefix
	Timeout    time.Duration // per-operation Valkey timeout
}

TokenBucketConfig defines the parameters for a token-bucket rate limiter.

func NewTokenBucketDefaultConfig added in v0.4.0

func NewTokenBucketDefaultConfig() TokenBucketConfig

NewTokenBucketDefaultConfig returns a TokenBucketConfig with sensible defaults.

Directories

Path Synopsis
internal
ratelimit
Package ratelimit provides shared infrastructure for Capacitor algorithm implementations.
Package ratelimit provides shared infrastructure for Capacitor algorithm implementations.
scripts
Package scripts embeds all Lua rate-limiting scripts and exposes them as ready-to-use *valkey.Lua values.
Package scripts embeds all Lua rate-limiting scripts and exposes them as ready-to-use *valkey.Lua values.

Jump to

Keyboard shortcuts

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