Skip to content
← ENGINEERING NOTES
NETWORKING6 min read

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.

go
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)
}
The minimal proxy loop

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.
APPLICATIONLIBRARYKERNELDRIVERHARDWARE
Request path from application down to the network stack

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.

TAGS

NetworkingProxiesSystems

RELATED

ENGINEERING NOTES

Things we built.Things we broke.Things we learned.