This project implements a simple in-memory rate limiter in Go using the Token Bucket algorithm. It is designed to sit in front of an HTTP handler as middleware and enforce per-client request limits.
The implementation is intentionally minimal and readable, focusing on correctness, concurrency safety, and clean HTTP semantics.
In real systems, allowing unlimited requests from a single client can:
- Overload your server
- Cause unfair resource usage
- Enable abuse (DDoS, brute force, scraping)
A rate limiter protects your service by controlling how many requests a client can make over time.
Each client (identified by IP) gets its own token bucket.
- Each bucket has:
- Capacity: maximum number of tokens
- Refill rate: tokens added per second
- Every request consumes tokens
- If enough tokens exist → request allowed
- If not → request rejected with
429 Too Many Requests
This allows:
- Short bursts (up to capacity)
- Sustained rate control (via refill rate)
Client Request
↓
Rate Limiter Middleware (Apex)
↓
Token Bucket (per client IP)
↓
Allow or Reject
↓
HTTP Handler (Your Service)
Manages all client buckets.
type Limiter struct {
buckets map[string]*TokenBucket
mutex sync.Mutex
}- One bucket per client key (IP)
- Thread-safe access
- Responsible for bucket creation and cleanup
Represents rate limits for a single client.
type TokenBucket struct {
capacity int
refillRate int
tokens int
lastRefillTime time.Time
lastSeen time.Time
mutex sync.Mutex
}Responsibilities:
- Track available tokens
- Refill tokens based on elapsed time
- Decide whether a request is allowed
The rate limiter is applied as HTTP middleware.
limitedHandler := rateLimiterMiddleware(baseHandler)It:
-
Extracts client IP
-
Checks allowance
-
Sets standard headers:
X-RateLimit-LimitX-RateLimit-RemainingRetry-After(on rejection)
Idle clients are removed to avoid memory leaks.
limiter.StartCleanUp(10*time.Minute, 1*time.Minute)- Runs periodically
- Deletes buckets not used recently
Clients are identified using:
X-Forwarded-For(proxy-safe)X-Real-IPRemoteAddrfallback
This makes the limiter usable behind reverse proxies.
HTTP 200 OK
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
HTTP 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
- In-memory and fast
- Per-client isolation
- Thread-safe with fine-grained locks
- Deterministic token refill
- Clean HTTP semantics
- Single-node only (no distributed coordination)
- Resets on process restart
- IP-based (can be spoofed without proper proxy config)
- Never (u have better options 😆)
go run main.goServer starts on:
http://localhost:8080
This project is meant to be simple but correct. It intentionally avoids frameworks and abstractions so the core rate-limiting logic is easy to reason about.
If you understand this implementation deeply, you understand the foundation of most production rate limiters.