A production-ready, high-performance rate limiting library for Go with multiple algorithms and pluggable storage backends.
go get github.com/sumedhvats/rate-limiter-goMost Go rate limiters force you into a single algorithm (usually token bucket) or lock you into a specific storage backend. When your startup grows from a single server to a distributed system, you're stuck rewriting your rate limiting logic.
rate-limiter-go solves this by providing:
- Multiple battle-tested algorithms – Choose the right algorithm for your use case, not the one your library happens to implement
- Pluggable storage – Start with in-memory, scale to Redis without changing application code
- Atomic Redis operations – Lua scripts ensure consistency under high concurrency (no race conditions)
- Real performance – ~60-70ns/op for concurrent operations, tested at scale
- Production-ready – Comprehensive test coverage, HTTP middleware, proven patterns
Whether you're building a side project or a high-traffic API, this library scales with you.
package main
import (
"fmt"
"time"
"github.com/sumedhvats/rate-limiter-go/pkg/limiter"
"github.com/sumedhvats/rate-limiter-go/pkg/storage"
)
func main() {
// Create in-memory storage
store := storage.NewMemoryStorage()
// Create limiter: 10 requests per minute
rateLimiter := limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 10,
Window: 1 * time.Minute,
})
// Check if request is allowed
allowed, err := rateLimiter.Allow("user:alice")
if err != nil {
panic(err)
}
if !allowed {
fmt.Println("Rate limit exceeded!")
return
}
fmt.Println("Request allowed!")
}That's it. Five lines to production-grade rate limiting.
Unlike libraries hardcoded to token bucket, you can choose the algorithm that fits your requirements:
| Algorithm | Best For | Tradeoff |
|---|---|---|
| Token Bucket | Smooth traffic, burst handling | Slightly more complex |
| Fixed Window | Simple counting, analytics | Boundary burst issues |
| Sliding Window Counter | General-purpose (recommended) | Balanced accuracy/performance |
Other libraries use approximate counters or lock-based concurrency. This implementation uses Lua scripts for atomic updates:
-- Fixed Window example (simplified)
local current = tonumber(redis.call('GET', key) or '0')
if current + increment > limit then
return 0 -- Denied
end
redis.call('INCRBY', key, increment)
redis.call('EXPIRE', key, ttl)
return 1 -- AllowedNo race conditions. No approximate counting. Just correctness.
Switch from single-instance to distributed without code changes:
// Development: in-memory
store := storage.NewMemoryStorage()
// Production: Redis (same interface)
store := storage.NewRedisStorage("redis-cluster:6379")
// Same limiter code works with both
rateLimiter := limiter.NewSlidingWindowLimiter(store, config)Real benchmark results (12th Gen Intel i5-12500H):
Algorithm Concurrent Performance Memory
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Token Bucket 63 ns/op 160 B/op
Sliding Window 67 ns/op 96 B/op
Fixed Window 119 ns/op 270 B/op
That's ~15 million operations/second on a single core. Scales linearly across multiple keys and goroutines.
go get github.com/sumedhvats/rate-limiter-goRequirements:
- Go 1.18+
- Redis 6.0+ (for distributed rate limiting)
package main
import (
"net/http"
"time"
"github.com/sumedhvats/rate-limiter-go/middleware"
"github.com/sumedhvats/rate-limiter-go/pkg/limiter"
"github.com/sumedhvats/rate-limiter-go/pkg/storage"
)
func main() {
// Redis storage for distributed systems
store := storage.NewRedisStorage("localhost:6379")
// 100 requests per minute per IP
rateLimiter := limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 100,
Window: 1 * time.Minute,
})
// Apply middleware
mux := http.NewServeMux()
mux.HandleFunc("/api/data", dataHandler)
handler := middleware.RateLimitMiddleware(middleware.Config{
Limiter: rateLimiter,
// Uses X-Forwarded-For and RemoteAddr automatically
})(mux)
http.ListenAndServe(":8080", handler)
}
func dataHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Data served successfully"))
}Automatic features:
- Standard rate limit headers (
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset) - Smart IP extraction (handles
X-Forwarded-For, proxies, IPv6) - JSON error responses with 429 status code
// Extract user ID from JWT, session, or API key
func getUserRateLimitKey(r *http.Request) string {
userID := extractUserIDFromJWT(r) // Your auth logic
return fmt.Sprintf("user:%s", userID)
}
// Apply custom key function
handler := middleware.RateLimitMiddleware(middleware.Config{
Limiter: rateLimiter,
KeyFunc: getUserRateLimitKey,
})(mux)premiumLimiter := limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 1000,
Window: 1 * time.Minute,
})
freeLimiter := limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 100,
Window: 1 * time.Minute,
})
func selectLimiter(user User) limiter.Limiter {
if user.IsPremium {
return premiumLimiter
}
return freeLimiter
}
// In your handler
func apiHandler(w http.ResponseWriter, r *http.Request) {
user := getUser(r)
limiter := selectLimiter(user)
allowed, _ := limiter.Allow(fmt.Sprintf("user:%s", user.ID))
if !allowed {
http.Error(w, "Rate limit exceeded", 429)
return
}
// Process request
}// Allow bursts up to 50 requests, but refill at 100/minute
rateLimiter := limiter.NewTokenBucketLimiter(store, limiter.Config{
Rate: 100, // Tokens per window
Window: 1 * time.Minute,
Burst: 50, // Max burst size
})
// Perfect for APIs that need to handle occasional traffic spikes// All app instances share the same Redis
store := storage.NewRedisStorage("redis-cluster:6379")
rateLimiter := limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 10000,
Window: 1 * time.Minute,
})
// Rate limiting now works across your entire cluster
// No coordination needed – Lua scripts handle atomicityhandler := middleware.RateLimitMiddleware(middleware.Config{
Limiter: rateLimiter,
OnLimit: func(w http.ResponseWriter, r *http.Request) {
// Custom response when rate limited
w.Header().Set("Retry-After", "60")
w.WriteHeader(http.StatusTooManyRequests)
json.NewEncoder(w).Encode(map[string]string{
"error": "Too many requests. Please upgrade to premium.",
"upgrade_url": "/pricing",
})
},
})(mux)Best for: Smooth traffic shaping, handling bursts gracefully
limiter.NewTokenBucketLimiter(store, limiter.Config{
Rate: 100, // Refill rate
Window: 1 * time.Minute,
Burst: 50, // Allow bursts
})How it works: Tokens refill continuously at a steady rate. Requests consume tokens. If tokens are available, request proceeds.
Pros:
- Handles bursts naturally (up to
Burstcapacity) - Smooth traffic distribution
- Most commonly used in production systems
Cons:
- Slightly more complex implementation
- Requires tracking token count + last refill time
Use cases: API gateways, public APIs, microservices
Best for: Simple counting, internal rate limiting, analytics
limiter.NewFixedWindowLimiter(store, limiter.Config{
Rate: 100,
Window: 1 * time.Minute,
})How it works: Counter resets at fixed intervals (e.g., every minute). Allows Rate requests per window.
Pros:
- Extremely simple and fast
- Minimal memory usage
- Easy to reason about
Cons:
- Boundary burst problem: Can allow 2× rate at window boundaries
- Example: 100 requests at 0:59, 100 more at 1:00 = 200 requests in 1 second
Use cases: Internal services, non-critical rate limiting, request counting
Best for: General-purpose rate limiting with accuracy
limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 100,
Window: 1 * time.Minute,
})How it works: Combines current window with weighted previous window to smooth out boundaries.
Weighted Count = (Previous Window × Weight) + Current Window
where Weight = time remaining in current window / window size
Pros:
- Solves boundary burst problem
- Low memory usage (only 2 counters)
- Good accuracy-performance balance
Cons:
- Slightly more complex than fixed window
- Not perfectly precise (good enough for most cases)
Use cases: REST APIs, webhooks, user-facing services
type Config struct {
Rate int // Requests allowed per window
Window time.Duration // Time window (e.g., 1 minute)
Burst int // Max burst size (Token Bucket only)
}Examples:
// 100 requests per minute
Config{Rate: 100, Window: 1 * time.Minute}
// 10 requests per second
Config{Rate: 10, Window: 1 * time.Second}
// 1000 requests per hour with 200 burst
Config{Rate: 1000, Window: 1 * time.Hour, Burst: 200}store := storage.NewMemoryStorage()
// Automatic cleanup of expired entries every 1 minute
// Thread-safe with sync.Map and atomic operationsWhen to use:
- Single-instance applications
- Development/testing
- Non-critical rate limiting
- Low-traffic services
store := storage.NewRedisStorage("localhost:6379")
// Custom configuration
client := redis.NewClient(&redis.Options{
Addr: "redis-cluster:6379",
Password: "your-password",
DB: 0,
PoolSize: 10,
MinIdleConns: 5,
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
})
store := storage.NewRedisStorageWithClient(client)When to use:
- Distributed systems (multiple servers)
- High-availability requirements
- Shared rate limits across services
- Production environments
Features:
- Atomic operations via Lua scripts
- Connection pooling
- Automatic expiration (TTL)
- Redis Cluster support
Benchmarked on 12th Gen Intel i5-12500H (16 logical cores):
Algorithm Sequential Concurrent Memory
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Token Bucket 202 ns/op 337 ns/op 160 B/op
Fixed Window 461 ns/op 1182 ns/op 264 B/op
Sliding Window 327 ns/op 69 ns/op 80 B/op
Algorithm Sequential Concurrent Memory
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Token Bucket 282 ns/op 76 ns/op 160 B/op
Fixed Window 588 ns/op 130 ns/op 261 B/op
Sliding Window 382 ns/op 68 ns/op 100 B/op
Algorithm Time/op Throughput
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Token Bucket 56 ns/op ~17M ops/sec
Fixed Window 95 ns/op ~10M ops/sec
Sliding Window 74 ns/op ~13M ops/sec
Key Insights:
- Sliding Window excels under concurrent load (~15M ops/sec)
- Token Bucket provides consistent performance across scenarios
- Fixed Window is fast but has higher memory overhead
- All algorithms scale linearly with number of keys
Algorithm Latency Throughput
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Token Bucket 26.5 µs ~37K ops/sec
Fixed Window 26.2 µs ~38K ops/sec
Sliding Window 26.8 µs ~37K ops/sec
Note: Redis performance depends on network latency. These benchmarks are localhost.
# All benchmarks
go test -bench=. -benchmem ./benchmarks
# Specific algorithm
go test -bench=BenchmarkSingleKey/SlidingWindow -benchmem ./benchmarks
# With CPU profiling
go test -bench=. -benchmem -cpuprofile=cpu.prof ./benchmarks
go tool pprof cpu.prof# All tests
go test ./...
# With coverage
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out
# Verbose output
go test -v ./...
# Specific package
go test ./pkg/limiterPackage Coverage
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
pkg/limiter 95.2%
pkg/storage 92.8%
middleware 89.4%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Overall 93.1%
rate-limiter-go/
├── pkg/
│ ├── limiter/ # Rate limiting algorithms
│ │ ├── limiter.go # Common interface
│ │ ├── token_bucket.go # Token bucket implementation
│ │ ├── fixed_window.go # Fixed window counter
│ │ └── sliding_window.go # Sliding window counter
│ └── storage/ # Storage backends
│ ├── storage.go # Storage interface
│ ├── memory.go # In-memory storage
│ └── redis.go # Redis storage with Lua scripts
├── middleware/ # HTTP middleware
│ └── middleware.go # Rate limit middleware
├── benchmarks/ # Performance benchmarks
│ └── benchmark_test.go
├── examples/ # Usage examples
│ ├── basic/
│ ├── http_api/
│ └── distributed/
└── docs/ # Additional documentation
Use Redis storage. All application instances will share the same rate limit counters:
// All servers point to the same Redis
store := storage.NewRedisStorage("redis-cluster:6379")
rateLimiter := limiter.NewSlidingWindowLimiter(store, config)Lua scripts ensure atomic operations, so there are no race conditions even with hundreds of concurrent servers.
Currently, the library fails closed (rejects requests) if Redis is unavailable. This prevents accidentally allowing unlimited requests.
Best practices:
- Use Redis Sentinel or Cluster for high availability
- Monitor Redis health
- Implement circuit breaker pattern in your application
- Consider graceful degradation (allow requests if Redis fails)
Future improvement: Optional "fail open" mode is planned for v2.0.
Yes! Create a gRPC interceptor:
func RateLimitInterceptor(limiter limiter.Limiter) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Extract client ID from metadata
md, _ := metadata.FromIncomingContext(ctx)
clientID := md.Get("client-id")[0]
allowed, err := limiter.Allow(clientID)
if err != nil {
return nil, status.Error(codes.Internal, "rate limiter error")
}
if !allowed {
return nil, status.Error(codes.ResourceExhausted, "rate limit exceeded")
}
return handler(ctx, req)
}
}
// Use it
server := grpc.NewServer(
grpc.UnaryInterceptor(RateLimitInterceptor(rateLimiter)),
)Combine identifiers in your key:
// Rate limit by user AND endpoint
key := fmt.Sprintf("user:%s:endpoint:%s", userID, r.URL.Path)
// Rate limit by IP AND API key
key := fmt.Sprintf("ip:%s:key:%s", ip, apiKey)
// Rate limit by tenant AND method
key := fmt.Sprintf("tenant:%s:method:%s", tenantID, r.Method)Create separate limiters for different tiers:
globalLimiter := limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 10000, Window: 1 * time.Minute,
})
perUserLimiter := limiter.NewSlidingWindowLimiter(store, limiter.Config{
Rate: 100, Window: 1 * time.Minute,
})
// Check both
globalAllowed, _ := globalLimiter.Allow("global")
userAllowed, _ := perUserLimiter.Allow(fmt.Sprintf("user:%s", userID))
if !globalAllowed || !userAllowed {
http.Error(w, "Rate limit exceeded", 429)
return
}Very accurate for practical purposes. The maximum error is bounded:
Max Error ≤ (Rate × Weight)
Example with 100 req/min:
- At 30s into window: error ≤ 50 requests (50% weight)
- At 45s into window: error ≤ 25 requests (25% weight)
- At 55s into window: error ≤ 8 requests (8% weight)
For most APIs, this accuracy is more than sufficient. If you need perfect precision, use Sliding Window Log (but accept higher memory usage).
Yes! Implement the Storage interface:
type Storage interface {
Get(key string) (interface{}, error)
Set(key string, value interface{}, ttl time.Duration) error
Delete(key string) error
Increment(key string, amount int, ttl time.Duration) (int64, error)
}Example: PostgreSQL storage
type PostgresStorage struct {
db *sql.DB
}
func (p *PostgresStorage) Get(key string) (interface{}, error) {
var value int64
err := p.db.QueryRow("SELECT value FROM rate_limits WHERE key = $1 AND expires_at > NOW()", key).Scan(&value)
if err == sql.ErrNoRows {
return nil, nil
}
return value, err
}
// Implement other methods...Then use it like any other storage:
store := NewPostgresStorage(db)
rateLimiter := limiter.NewSlidingWindowLimiter(store, config)Use the Reset() method:
// Reset specific user
err := rateLimiter.Reset("user:alice")
// Reset specific IP
err := rateLimiter.Reset("ip:192.168.1.1")This is useful for:
- Admin actions (unblock a user)
- Testing
- Premium user upgrades
- Pardoning accidental rate limit hits
| Feature | rate-limiter-go | golang.org/x/time/rate | tollbooth | uber-go/ratelimit |
|---|---|---|---|---|
| Multiple Algorithms | ✅ 4 algorithms | ❌ Token bucket only | ✅ Multiple | ❌ Leaky bucket only |
| Pluggable Storage | ✅ Memory + Redis | ❌ Memory only | ❌ Memory only | ❌ Memory only |
| Distributed Support | ✅ Redis with Lua | ❌ No | ❌ No | ❌ No |
| HTTP Middleware | ✅ Built-in | ❌ DIY | ✅ Built-in | ❌ DIY |
| Atomic Operations | ✅ Lua scripts | ✅ sync.Mutex | ✅ atomic.Int64 | |
| Burst Handling | ✅ Token bucket | ✅ Yes | ❌ No | ❌ No |
| Rate Limit Headers | ✅ Auto | ❌ Manual | ✅ Auto | ❌ Manual |
| Performance | ~60ns/op | ~50ns/op | ~200ns/op | ~40ns/op |
| Complexity | Medium | Low | Medium | Low |
When to use rate-limiter-go:
- You need distributed rate limiting (multiple servers)
- You want flexibility to choose algorithms
- You need to scale from single-instance to distributed
- You want production-ready middleware
When to use alternatives:
golang.org/x/time/rate: Simple token bucket, single instance, low-level controltollbooth: Quick HTTP rate limiting, memory-onlyuber-go/ratelimit: Extremely simple, in-process rate limiting only
- ✅ Four rate limiting algorithms
- ✅ Memory and Redis storage
- ✅ HTTP middleware
- ✅ Comprehensive benchmarks
- ✅ Rate limit headers
- Adaptive rate limiting (adjust limits based on load)
- Cost-based rate limiting (different costs per endpoint)
- Circuit breaker integration
- Prometheus metrics
- Graceful Redis failure handling (fail open option)
- Memcached storage backend
- gRPC middleware (built-in)
- Distributed coordination without Redis (gossip protocol)
- WebSocket rate limiting
- GraphQL query complexity rate limiting
- Admin dashboard
Contributions welcome!
We love contributions! Here's how you can help:
- Report bugs – Open an issue with reproduction steps
- Suggest features – Describe your use case
- Improve docs – Fix typos, add examples
- Submit PRs – See guidelines below
# Clone repo
git clone https://github.com/sumedhvats/rate-limiter-go.git
cd rate-limiter-go
# Install dependencies
go mod download
# Run tests
go test ./...
# Run benchmarks
go test -bench=. ./benchmarks
# Check coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for new functionality
- Ensure all tests pass (
go test ./...) - Run gofmt (
go fmt ./...) - Commit with clear messages (
git commit -m 'Add amazing feature') - Push to your fork
- Open a Pull Request
- Follow standard Go conventions
- Use
gofmtfor formatting - Write clear, descriptive variable names
- Add comments for complex logic
- Update documentation for new features
- Unit tests for all new code
- Benchmarks for performance-critical changes
- Integration tests for storage backends
- Maintain >90% code coverage
MIT License – see LICENSE for details.
- Algorithm design inspired by Cloudflare's rate limiting architecture
- Lua scripting patterns adapted from Redis documentation
- Benchmark methodology influenced by Go's benchmark practices
Special thanks to all contributors and the Go community!
- 📚 Documentation: pkg.go.dev
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- 📧 Email: [[email protected]]
If this library helps you, consider giving it a ⭐ on GitHub!