A composable, backend-agnostic rate-limiting library for Go.
Combines Token Bucket and Leaky Bucket algorithms into a flexible, production-ready toolkit.
FluxGuard is a Go rate-limiting library designed with library consumers in mind:
- Zero stdout output — the library never writes to the terminal unless you explicitly provide a logger.
- Backend-agnostic — plug in Redis for distributed deployments or the built-in in-memory store for single-instance services and tests.
- Safe constructors — every constructor validates its inputs and returns an
errorinstead of panicking. - Composable — use each algorithm independently or combine them via
HybridLimiter.
Allows up to capacity requests in a burst, then refills at rate tokens per second. Ideal for APIs that need to tolerate short bursts while enforcing an average throughput limit.
Tokens ──── refill (rate/s) ────► bucket (cap N)
│
request ─────┤ token available? → allow
│ no token? → deny (429)
Queues incoming requests and releases them at a constant rate. Excess requests beyond capacity are rejected immediately. Ideal for smoothing bursty traffic into a steady output stream.
Request ──► queue (cap N) ──── emit (rate/s) ──► handler
│ full?
└──► deny (429)
A two-stage pipeline that provides both burst tolerance and output smoothing in a single call.
Request ──► [Token Bucket] ──pass──► [Leaky Bucket] ──pass──► handler
│ deny │ deny
└──► 429 └──► 429
Requires Go 1.21 or later (for log/slog).
go get github.com/JackBerck/fluxguard@latestpackage main
import (
"context"
"fmt"
"log"
"github.com/JackBerck/fluxguard/pkg/limiter"
"github.com/JackBerck/fluxguard/pkg/storage"
)
func main() {
store := storage.NewRedisStorage("localhost:6379", "")
// capacity=10 tokens, refill rate=2 tokens/second
tb, err := limiter.NewTokenBucket(store, 10, 2)
if err != nil {
log.Fatal(err)
}
ok, err := tb.Allow(context.Background(), "user-123")
if err != nil {
log.Fatal(err)
}
if !ok {
fmt.Println("rate limited")
}
}// queue capacity=5, emit rate=1 request/second
lb, err := limiter.NewLeakyBucket(store, 5, 1)
if err != nil {
log.Fatal(err)
}
ok, err := lb.Allow(r.Context(), clientIP)hl, err := limiter.NewHybridLimiter(store, limiter.HybridConfig{
TokenCapacity: 10, // burst up to 10
TokenRate: 2, // refill 2 tokens/s
LeakyCapacity: 5, // queue up to 5
LeakyRate: 1, // emit 1 req/s
})
if err != nil {
log.Fatal(err)
}
ok, err := hl.Allow(r.Context(), clientIP)FluxGuard is silent by default. To enable structured logging, pass any value that satisfies limiter.Logger — a *slog.Logger works out of the box via limiter.NewSlogLogger:
import "log/slog"
logger := limiter.NewSlogLogger(slog.Default())
tb, err := limiter.NewTokenBucket(store, 10, 2,
limiter.WithTokenBucketLogger(logger),
)You can also implement limiter.Logger yourself to integrate with any logging framework (Zap, Zerolog, etc.).
store := storage.NewMemoryStorage()
tb, _ := limiter.NewTokenBucket(store, 5, 1)func rateLimitMiddleware(allow func(context.Context, string) (bool, error)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ok, err := allow(r.Context(), r.RemoteAddr)
if err != nil && err != context.Canceled {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if !ok {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
http.Handle("/api/data", rateLimitMiddleware(hl.Allow)(myHandler))| Backend | Type | Use case |
|---|---|---|
storage.NewMemoryStorage() |
In-process | Single instance, testing |
storage.NewRedisStorage(addr, password) |
Redis (distributed) | Multi-instance, production |
Implement the storage.Storage interface to use any other backend (Memcached, DynamoDB, etc.).
| Technology | Role |
|---|---|
| Go | Core language |
| Redis + go-redis | Distributed state via atomic Lua scripts |
| log/slog | Structured logging adapter |
| k6 | Load testing (k6/ directory) |
fluxguard/
├── pkg/
│ ├── limiter/ # Rate limiter implementations
│ │ ├── token.go # TokenBucketLimiter
│ │ ├── leaky.go # LeakyBucketLimiter
│ │ ├── hybrid.go # HybridLimiter + HybridConfig
│ │ └── logger.go # Logger interface + SlogLogger adapter
│ └── storage/ # Storage interface and backends
│ ├── interface.go
│ ├── memory.go # In-memory backend
│ └── redis.go # Redis backend (atomic Lua scripts)
├── test/ # Integration-style tests (package test)
├── k6/ # k6 load-test scripts
├── cmd/example/ # Runnable demo server
└── .private/ # Local notes (git-ignored)
└── VERSIONING.md
go test ./...The
test/package usesstorage.MemoryStorageso no Redis instance is required.
Start Redis locally, then:
go run ./cmd/exampleEndpoints available at http://localhost:8080:
| Endpoint | Algorithm |
|---|---|
/api/data/token |
Token Bucket |
/api/data/leaky |
Leaky Bucket |
/api/data/hybrid |
Hybrid |
Run the k6 load test:
k6 run k6/loadtest_hybrid.js