❯ Infra · Platform · 2025
GoGateway
A production-grade reverse proxy built from scratch in Go with only three external dependencies and a 5.4MB Alpine image.
The problem
The team was paying for a commercial gateway that did one thing well and a dozen things nobody asked for. Observability was the worst part: per-route latency and error rates meant stitching log shipping together and hoping. For a platform that promises fast failover, hope is not an SLO.
The approach
Build the gateway from scratch in Go with only the standard library — net/http plus httputil.ReverseProxy and three external dependencies — with the Prometheus exporter as a first-class citizen instead of an afterthought. A gateway is a thin, hot path: fewer dependencies means fewer supply-chain surprises, and the whole binary compiles to a ~5.4MB Alpine image — small enough to deploy everywhere, fast enough to sit in front of every service.
Architecture
- Clients
- GoGateway
- JWT / API-Key Auth
- Redis Rate Limiter
- Circuit Breaker
- Upstream Services
- Prometheus
FLOWS
- ClientsHTTPGoGateway
- GoGatewayverifyJWT / API-Key Auth
- GoGatewaythrottleRedis Rate Limiter
- GoGatewayper-routeCircuit Breaker
- Circuit BreakerproxyUpstream Services
- GoGatewaycountersPrometheus
Clients hit the gateway over HTTP. JWT auth (HS256/RS256 with issuer whitelisting and claim forwarding via X-User-ID / X-User-Claims) or API-key auth (SHA-256 hashed key stores with tiers and revocation) runs first. Then a Redis-backed rate limiter — an atomic Lua script — applies standard X-RateLimit-* and Retry-After headers and falls back to an in-memory limiter if Redis is unreachable. Then a three-state circuit breaker (Closed → Open → Half-Open) with per-route failure thresholds and probe-based recovery. Then pluggable load balancing — round-robin or least-connections — across multi-upstream routes with wildcard path matching.
Observability is the point of the project: a custom Prometheus exporter with zero client-library dependencies — request counters, latency histograms, active-connection gauges — plus JSON structured logging via log/slog and X-Request-Id tracing. Ship-readiness was part of the build: a multi-stage Dockerfile, Kubernetes manifests (2-replica Deployment, probes, ClusterIP, ConfigMap), and docker-compose for local dev. The test suite is table-driven throughout, run with go test -race on every concurrency path.
Key decisions
Prometheus as the native output
The gateway's internal counters are the exporter's source — no translation layer, no drift between behavior and dashboards.
Small binary as a feature
5.4MB Alpine means millisecond cold starts and a blast radius the size of a pocket.
Rate limiter that degrades, not dies
The Lua script is atomic when Redis is up; the in-memory fallback keeps traffic moving when it isn't.
Race-tested from day one
Table-driven tests with go test -race on every concurrency path.
What I'd do differently
The first version exported histograms with default bucket sizes, which made p95 read as noise; buckets should have been designed from the p99 target backwards. Route configuration also deserves a first-class schema — wildcard matching and per-route overrides accumulate edge cases fast, and the config file ended up with more corner cases than the proxy itself.