Distributed Rate Limiter & API Gateway
Build a lightweight API gateway in Go: it sits in front of your backend services, forwards each request to the right one with httputil.ReverseProxy, and enforces rate limits per API key that are shared across several gateway instances through Redis, so a limit of 100 requests per minute holds no matter which instance handles each request. It adds standard rate limit headers, counts requests and rejections for monitoring, falls back to a local limiter when Redis is unavailable, and is load tested with several instances to prove the limits hold. You'll learn reverse proxying in Go, the token bucket algorithm, atomic Redis Lua scripts with go-redis, per-client configuration, graceful degradation, metrics and load testing.
- Estimate
- ~13h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
Redis · net/http/httputil · go-redis
Project roadmap
- 01
Build the reverse proxy
~2hRead a config file (YAML) with routes: a path prefix and a backend URL each. Create an httputil.ReverseProxy per backend (with the Rewrite function to set the target and X-Forwarded headers), with timeouts on the transport, and route requests by the longest matching prefix, returning 404 for unknown paths and 502 when a backend is down. Write two tiny backend services to test with. Done when: requests reach the right backend through the gateway.
- 02
Add Redis-backed rate limiting
~2.5hWrite a token bucket as a Lua script that refills based on Redis's TIME, takes a token if available and returns the decision, remaining tokens and wait time, stored with an expiry. Run it with go-redis (loaded once and called with EVALSHA). Identify clients by an API key header, and return 429 when the limit is exceeded. Done when: requests over the limit get 429, and limits are shared by two gateway instances.
- 03
Configure limits per key
~1.5hLet the config file set default limits, limits per API key or plan, and per route (such as stricter limits on expensive endpoints). Add RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and Retry-After headers. Reject unknown API keys with 401. Done when: different keys and routes get their configured limits, with correct headers.
- 04
Add logging and metrics
~1.5hLog each request (key id, route, backend, status, latency and whether it was rate limited) with log/slog, and expose Prometheus metrics with client_golang: requests per route and status, rejections per key, backend latency and Redis errors. Done when: /metrics shows requests and rejections, and logs have one line per request.
- 05
Handle Redis failures
~1.5hPut a short timeout on Redis calls. When Redis is unreachable, switch to an in-memory token bucket per instance (with the limit divided by the configured number of instances), log the switch once, and switch back when Redis recovers (checked periodically). Done when: stopping Redis keeps the gateway serving with local limits, and starting it again restores shared limits.
- 06
Load test the gateway
~2hRun three gateway instances and Redis with Docker Compose, and load test on your own machine with a tool such as vegeta or k6 across all instances with a few API keys. Check the total allowed requests per key never exceeds its limit, and record throughput and added latency in README.md. Done when: README.md shows the limits held under load and the gateway's overhead.
- 07
Test it
~2hWrite tests with httptest backends and a real Redis (Testcontainers or miniredis for unit tests): routing, headers, per-key and per-route limits, concurrent requests from many goroutines never exceeding a limit, 401 for unknown keys, and the fallback when Redis is down. Run with -race. Done when: go test -race ./... passes.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack