Lightweight HTTP middleware that enforces per-client rate limits using Go’s token-bucket (golang.org/x/time/rate).
Clients are identified by X-API-Key, then X-Forwarded-For / X-Real-IP, falling back to the remote IP.
- per-client limiter (
key → *rate.Limiter) with idle eviction - Rate-limit headers on all responses:
X-RateLimit-Limit: configured steady rate (req/sec)X-RateLimit-Remaining: approx tokens leftRetry-After: on429, seconds to wait before retrying
- 2 behaviors :
- Blocking mode (optional) : wait up to
maxWaitfor a token, then proceed - Non-blocking : fail fast with
429when bucket is empty
- Blocking mode (optional) : wait up to
- minimal drop-in middleware for
net/http.
# Run (dev)
go run .
# Build binary (Windows example)
go build -o bin/rl-demo.exeServer listens on :8080 and exposes a single GET / endpoint that replies with ok (when allowed)
//per client policy
const (
perClientRPS = 5 //steady tokens/sec per client
perClientBurst = 10 //short burst capacity
)
//behavior toggle
var (
useBlocking = true //true = delay up to maxWait & false = fail fast
maxWait = 300 * time.Millisecond //per req eait budget in blocking mode
)
//idle eviction (in main)
store.startCleanup(5*time.Minute, 1*time.Minute) //idle TTL, scan intervalX-API-Key- first IP in
X-Forwarded-For X-Real-IP- connection's
remote IP
Only trust X-Forwarded-For / X-Real-IP if your service is behind a trusted proxy/load-balancer that sets them.
# Will show headers
curl.exe -i http://localhost:8080/# Will return Retry-After
1..30 | % { curl.exe -i -s http://localhost:8080/ | Select-String "HTTP/|X-Rate|Retry-After" }# Install & add to path
go install github.com/rakyll/hey@latest
$env:Path += ";$env:USERPROFILE\go\bin"
# Hammer test
hey -n 200 -c 20 http://localhost:8080/
# Paced test
hey -z 10s -q 5 -c 2 http://localhost:8080/
# Per client fairness
# Terminal A
hey -z 10s -q 5 -c 2 -H "X-API-Key: A" http://localhost:8080/
# Terminal B
hey -z 10s -q 5 -c 2 -H "X-API-Key: B" http://localhost:8080/
# Blocking mode
# with userBlocking = true and maxWait - 300ms, short spiked are absorbed, sustained overload still yields some 429s
# to see fewer 429s on a big burst, temporarily increase maxWait, then
hey -n 200 -c 20 http://localhost:8080/
# you'll see more 200s and higher latency- for each client key there is a token-bucket limiter
- every request:
- derives client key -> fetches/creates limiter, updates lastSeen
- Blocing mode :
lim.Wait(ctx)up tomaxWait- if context times out, set headers +
Retry-After-> 429 - else, proceed
- if context times out, set headers +
- Non blocing :
lim.Allow()- if false, set headers +
Retry-After-> 429
- if false, set headers +
- on success-> set X-RateLimit- + Retry-After & call next handler
- a background go routine evicts idle clients so map doesn't grow forever
add blocking mode (use limiter.Wait to delay)doneroute-level configs (different limits per endpoint)donemetrics for allowed/denied/latencydone- redis backend ? to share limits across instances
- maybe turn this into a cli or small library package for drop-in use