A reverse proxy built from scratch using Go's standard library. Features round-robin load balancing, in-memory response caching, per-IP rate limiting, and background health checks.
Client → :8080 Proxy → [Rate Limiter] → [Cache] → [Load Balancer] → Backend :8081
→ Backend :8082
Request flow:
- Rate limit check (per-IP, fixed window) — rejects with
429if exceeded - Cache lookup (GET requests only) — returns cached response on hit
- Round-robin backend selection — skips unhealthy backends
- Forward request via
httputil.ReverseProxy - Cache response (GET 200 only) with configurable TTL
docker compose up --buildThis starts 2 backend instances and 1 proxy on :8080.
# Terminal 1 — Backend on :8081
go run cmd/backend/main.go
# Terminal 2 — Backend on :8082 (optional)
PORT=8082 go run cmd/backend/main.go
# Terminal 3 — Proxy on :8080
go run cmd/proxy/main.gocurl http://localhost:8080/
curl http://localhost:8080/about
curl -X POST -H "Content-Type: application/json" -d '{"name":"test"}' http://localhost:8080/submit
curl http://localhost:8080/listgo test ./... -race25 tests covering all components with race detection:
| Component | Tests |
|---|---|
| Rate Limiter | 8 (concurrency, window reset, per-IP isolation, boundary) |
| Cache | 6 (TTL expiry, cleanup, overwrite) |
| Load Balancer | 7 (round-robin, unhealthy skip, fallback, concurrency) |
| Health Checker | 3 (down on failure, up on success, down on 5xx) |
| Proxy Handler | 4 (forwarding, caching, rate limiting, headers) |
| Config | 3 (valid load, missing file, invalid YAML) |
All settings in config.yaml:
proxy:
port: 8080
backends:
- url: http://localhost:8081
- url: http://localhost:8082
cache:
ttl_seconds: 30
rate_limit:
requests: 5
window_seconds: 10
health_check:
interval_seconds: 10
timeout_seconds: 2
path: /cmd/
proxy/main.go # Proxy entry point
backend/main.go # Backend entry point
internal/
proxy/
proxy.go # Core handler (wires all components)
balancer.go # Round-robin load balancer with atomic health flags
health.go # Background health checker (goroutine)
cache.go # In-memory cache with TTL + background cleanup
limiter.go # Per-IP fixed-window rate limiter
middleware.go # Request logging middleware
*_test.go # Tests for each component
config/
config.go # YAML config parser
config_test.go
backend/
server.go # Backend HTTP server
handlers.go # Route handlers (/, /about, /submit, /list)
config.yaml # Local config
config.docker.yaml # Docker config (uses container hostnames)
docker-compose.yml # Runs proxy + 2 backends
Dockerfile.proxy
Dockerfile.backend
sync/atomic.Boolfor backend health — lock-free reads during load balancinghttptest.NewRecorderto intercept responses for caching before writing to client- Background goroutines for health checks, cache cleanup, and rate limit resets
- Multi-stage Docker builds — final images are ~15MB (Alpine + static binary)
- No external dependencies beyond
gopkg.in/yaml.v3for config parsing