capacitor

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Feb 18, 2026 License: EUPL-1.2 Imports: 10 Imported by: 0

README

Capacitor

A leaky-bucket rate limiter for Go, backed by Valkey. Atomic bucket logic runs server-side via a Lua script, making it safe for distributed deployments. Ships with drop-in net/http middleware.

Features

  • Atomic leaky-bucket algorithm executed in a single Valkey round-trip
  • Standard func(http.Handler) http.Handler middleware — works with http.ServeMux, chi, gorilla/mux, and 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)
  • 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.

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.New(client, capacitor.Config{
		KeyPrefix: "rl",
		Capacity:  10,
		LeakRate:  1.0, // 1 token per second
		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))
}

Configuration

Field Type Description
KeyPrefix string Prefix for Valkey keys (e.g. "rl"rl:<uid>)
Capacity int64 Maximum tokens in the bucket
LeakRate float64 Tokens drained per second
Timeout time.Duration Per-call Valkey timeout

Middleware Options

WithKeyFunc

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

// Rate-limit by API key header.
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 capacitor.New:

Option Description
WithLogger(logger) Structured logger (*slog.Logger)
WithFallback(strategy) FallbackFailOpen (default) or FallbackFailClosed
WithMetrics(collector) Optional MetricsCollector implementation

Response Headers

Every response includes standard rate-limit headers:

Header Description
RateLimit-Limit Bucket capacity
RateLimit-Remaining Tokens remaining
RateLimit-Reset Seconds until a token becomes available (denied requests only)
Retry-After Same value as RateLimit-Reset (denied requests only)

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

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrEmptyUID     = errors.New("capacitor: uid must not be empty")
	ErrEvalResponse = errors.New("capacitor: invalid eval response")
)

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 instance.

Types

type Capacitor

type Capacitor struct {
	// contains filtered or unexported fields
}

capacitor implements leaky bucket using valkey-go native client.

func New

func New(client valkey.Client, cfg Config, opts ...Option) *Capacitor

func (*Capacitor) Attempt

func (s *Capacitor) Attempt(ctx context.Context, uid string) (Result, error)

Attempt checks whether the request identified by uid is allowed. On Valkey errors it returns a fallback result and the underlying error.

func (*Capacitor) Close

func (s *Capacitor) Close()

Close gracefully shuts down the client.

func (*Capacitor) HealthCheck

func (s *Capacitor) HealthCheck(ctx context.Context) error

HealthCheck verifies connectivity.

type Config

type Config struct {
	Capacity  int64
	LeakRate  float64
	KeyPrefix string
	Timeout   time.Duration
}

func DefaultConfig

func DefaultConfig() Config

type FallbackStrategy

type FallbackStrategy int
const (
	FallbackFailOpen FallbackStrategy = iota
	FallbackFailClosed
)

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 MetricsCollector

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

type MiddlewareOption

type MiddlewareOption func(*mw)

MiddlewareOption configures the HTTP middleware.

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.

type Option

type Option func(*Capacitor)

func WithFallback

func WithFallback(s FallbackStrategy) Option

func WithLogger

func WithLogger(logger *slog.Logger) Option

func WithMetrics

func WithMetrics(m MetricsCollector) Option

type Result

type Result struct {
	Allowed    bool
	Remaining  int64
	Limit      int64
	RetryAfter time.Duration // Zero when allowed.
}

Jump to

Keyboard shortcuts

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