Skip to content

Drop-in per-client rate limiting for Go (net/http): token bucket, burst control, Retry-After, optional waits.

Notifications You must be signed in to change notification settings

ndrada/api-rate-limiter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

API Rate Limiter (Go) : Per-Client Token Bucket (+ optional blocking)

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.


Features

  • 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 left
    • Retry-After: on 429, seconds to wait before retrying
  • 2 behaviors :
    • Blocking mode (optional) : wait up to maxWait for a token, then proceed
    • Non-blocking : fail fast with 429 when bucket is empty
  • minimal drop-in middleware for net/http.

Quick start

# Run (dev)
go run .

# Build binary (Windows example)
go build -o bin/rl-demo.exe

Server listens on :8080 and exposes a single GET / endpoint that replies with ok (when allowed)


Configuration

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

Client key priority:

  1. X-API-Key
  2. first IP in X-Forwarded-For
  3. X-Real-IP
  4. 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.

Testing

1. By hand

# Will show headers
curl.exe -i http://localhost:8080/

2. Force a 429

# Will return Retry-After
1..30 | % { curl.exe -i -s http://localhost:8080/ | Select-String "HTTP/|X-Rate|Retry-After" }

3. Load test with hey

# 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

How it works

  • 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 to maxWait
      • if context times out, set headers + Retry-After -> 429
      • else, proceed
    • Non blocing : lim.Allow()
      • if false, set headers + Retry-After -> 429
    • on success-> set X-RateLimit- + Retry-After & call next handler
  • a background go routine evicts idle clients so map doesn't grow forever

Next steps/ideas

  • add blocking mode (use limiter.Wait to delay) done
  • route-level configs (different limits per endpoint) done
  • metrics for allowed/denied/latency done
  • redis backend ? to share limits across instances
  • maybe turn this into a cli or small library package for drop-in use

About

Drop-in per-client rate limiting for Go (net/http): token bucket, burst control, Retry-After, optional waits.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages