Building a Reverse Proxy From Scratch
What nginx and Envoy are actually doing under their config files — routing, connection handling and failure modes, implemented directly.
BY ASHLIN DARIUS GOVINDASAMY
A reverse proxy config file hides a surprising amount of engineering: connection lifecycle management, upstream selection, header rewriting, timeout and retry semantics, and backpressure. Writing one from scratch — even a small one — makes every one of those decisions explicit.
The shape of the problem
At its core, a proxy accepts an inbound connection, decides where it goes, opens (or reuses) an outbound connection, and copies bytes in both directions until one side closes. Everything else — TLS termination, routing rules, load balancing — sits around that loop.
func handle(client net.Conn, upstream string) {
defer client.Close()
backend, err := net.Dial("tcp", upstream)
if err != nil {
return
}
defer backend.Close()
go io.Copy(backend, client)
io.Copy(client, backend)
}Where the real engineering happens
- Routing — matching a request to an upstream by host, path or header, before a single byte of body is read.
- Timeouts on both legs of the connection, so a slow client can't pin resources indefinitely.
- Header handling — what gets forwarded, stripped, or rewritten (X-Forwarded-For, Host) between hops.
- Failure handling — what the proxy returns when the upstream is down or slow, and whether it retries.
A production proxy adds connection pooling, health checks and observability on top of this — but the loop above is the actual mechanism everything else wraps around.
NEWSLETTER
Get the engineering notes in your inbox.
Kernels, GPUs, networks, AI infrastructure — and the things we broke along the way. No spam, one click to leave.
We store only your email address, to send you new articles. See our privacy policy.
TAGS
RELATED