Skip to content

Sumedhvats/rate-limiter-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rate-limiter-go

A production-ready, high-performance rate limiting library for Go with multiple algorithms and pluggable storage backends.

go get github.com/sumedhvats/rate-limiter-go

Go Reference Go Report Card License: MIT


Why This Library?

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


Quick Start

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.


What Makes This Different?

1. Algorithm Flexibility

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

2. Atomic Redis Operations

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  -- Allowed

No race conditions. No approximate counting. Just correctness.

3. Storage Backend Abstraction

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)

4. Performance That Scales

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.


Installation

go get github.com/sumedhvats/rate-limiter-go

Requirements:

  • Go 1.18+
  • Redis 6.0+ (for distributed rate limiting)

Usage Examples

HTTP API with Middleware

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

Per-User Rate Limiting

// 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)

Tiered Rate Limiting (Free vs Premium)

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
}

Token Bucket with Burst Handling

// 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

Distributed Rate Limiting (Multiple Servers)

// 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 atomicity

Custom Error Handling

handler := 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)

Algorithm Deep Dive

When to Use Each Algorithm

Token Bucket (Recommended for APIs)

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 Burst capacity)
  • 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


Fixed Window Counter (Simplest)

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


Sliding Window Counter (Recommended for Production)

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


Visual Comparison


Configuration Reference

Config Struct

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}

Storage Configuration

Memory Storage

store := storage.NewMemoryStorage()
// Automatic cleanup of expired entries every 1 minute
// Thread-safe with sync.Map and atomic operations

When to use:

  • Single-instance applications
  • Development/testing
  • Non-critical rate limiting
  • Low-traffic services

Redis Storage

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

Performance Benchmarks

Memory & Allocation Efficiency (In-Memory) Performance Scalibility (By concurrency) Performance Scalability (by Number of Keys) Storage Backend Performance (Memory vs. Redis) Algorithm Performance (Concurrent Workloads)

Real-World Results

Benchmarked on 12th Gen Intel i5-12500H (16 logical cores):

Single Key Performance

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

Multiple Keys (Realistic Load)

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

Scalability (10K Keys)

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

Redis Performance

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.

Run Benchmarks Yourself

# 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

Testing

Run Tests

# 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/limiter

Test Coverage

Package                Coverage
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
pkg/limiter           95.2%
pkg/storage           92.8%
middleware            89.4%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Overall               93.1%

Project Structure

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

FAQ

How do I handle distributed rate limiting?

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.


What happens if Redis goes down?

Currently, the library fails closed (rejects requests) if Redis is unavailable. This prevents accidentally allowing unlimited requests.

Best practices:

  1. Use Redis Sentinel or Cluster for high availability
  2. Monitor Redis health
  3. Implement circuit breaker pattern in your application
  4. Consider graceful degradation (allow requests if Redis fails)

Future improvement: Optional "fail open" mode is planned for v2.0.


Can I use this with gRPC?

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)),
)

How do I rate limit by multiple dimensions?

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
}

How accurate is Sliding Window Counter?

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


Can I implement custom storage backends?

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)

How do I reset rate limits for a user?

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

Comparison with Other Libraries

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 ⚠️ Approximate ✅ 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 control
  • tollbooth: Quick HTTP rate limiting, memory-only
  • uber-go/ratelimit: Extremely simple, in-process rate limiting only

Roadmap

v0.1.x (Current)

  • ✅ Four rate limiting algorithms
  • ✅ Memory and Redis storage
  • ✅ HTTP middleware
  • ✅ Comprehensive benchmarks
  • ✅ Rate limit headers

v1.0 (Planned)

  • 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)

v2.0 (Future)

  • Distributed coordination without Redis (gossip protocol)
  • WebSocket rate limiting
  • GraphQL query complexity rate limiting
  • Admin dashboard

Contributions welcome!


Contributing

We love contributions! Here's how you can help:

Ways to Contribute

  1. Report bugs – Open an issue with reproduction steps
  2. Suggest features – Describe your use case
  3. Improve docs – Fix typos, add examples
  4. Submit PRs – See guidelines below

Development Setup

# 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

PR Guidelines

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for new functionality
  4. Ensure all tests pass (go test ./...)
  5. Run gofmt (go fmt ./...)
  6. Commit with clear messages (git commit -m 'Add amazing feature')
  7. Push to your fork
  8. Open a Pull Request

Code Style

  • Follow standard Go conventions
  • Use gofmt for formatting
  • Write clear, descriptive variable names
  • Add comments for complex logic
  • Update documentation for new features

Testing Requirements

  • Unit tests for all new code
  • Benchmarks for performance-critical changes
  • Integration tests for storage backends
  • Maintain >90% code coverage

License

MIT License – see LICENSE for details.


Acknowledgments

Special thanks to all contributors and the Go community!


Support


If this library helps you, consider giving it a ⭐ on GitHub!

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages