8 min read · Guide · Networking
How it works · Networking

Reverse proxies, the layer that sits in front of your servers.

A traffic cop at the edge of your service. It does five jobs, mostly. Once you've named them, the configs of Nginx, Envoy, HAProxy, and Caddy stop looking like five different things.

Parts01–10 InteractiveFunction picker PrereqHTTP · TLS · L4 vs L7

What is a reverse proxy?

It accepts inbound, forwards to private backends.

A reverse proxy sits in front of one or more backend servers, receiving client requests and forwarding them to a backend it picks. The big three — NGINX, HAProxy, Envoy — handle TLS termination, load balancing, request routing, caching, rate limiting, and observability. Apple, Cloudflare, AWS, and Google all run reverse proxies at planetary scale.

A forward proxy sits in front of clients and reaches arbitrary servers — corporate egress filters, Squid, the proxy your university used. A reverse proxy sits in front of servers and accepts arbitrary clients. The clients have no idea which backend is serving them; the backends have no idea which client originated the call. The proxy bridges both worlds.

Almost every internet-facing application service runs behind one. The proxy is where TLS terminates, where routing decisions live, where rate limits enforce, and where the bulk of "edge" features get added — without touching application code.


The five jobs a reverse proxy does

Pick a function, see the config that turns it on.

A reverse proxy is rarely deployed for one job alone. But naming the five separately — including load balancing — and recognising each by its config block makes it much easier to read someone else's setup.

Function 01 · TLS termination

Decrypt at the edge.

The proxy holds the certificate and decrypts inbound traffic; backends speak plaintext on a private network. Centralizes cert management, ALPN selection, OCSP stapling, and TLS protocol upgrades. Often the only function deployed for "small" reverse proxies.

ssl_certificate /etc/ssl/cert.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_stapling on;
ssl_session_cache shared:SSL:50m;

L4 vs L7: moving bytes or reading requests

An L4 proxy moves bytes; an L7 proxy reads the request.

An L4 (transport) proxy moves bytes. It sees source IP, destination IP, ports, and the TCP stream — but not the HTTP path or method. Cheap, fast, opaque to TLS. AWS NLB, HAProxy in TCP mode, Envoy in TCP listener mode all do this.

An L7 (application) proxy understands the protocol — typically HTTP. It can route by path, rewrite headers, retry on 5xx, even rewrite responses. Also more expensive: it must terminate TLS to peek inside, buffer the request to inspect, and re-emit. AWS ALB, Nginx, Caddy, Envoy in HTTP listener mode.

Choose by need. If you need to route by path, you need L7. If raw TCP is fine and TLS pass-through matters, L4 is cheaper.


Reverse proxy vs load balancer vs API gateway

Same box on the diagram, three different jobs.

On a whiteboard, all three are the same rectangle: traffic in the front, decisions in the middle, traffic out the back. The names describe jobs, not software — and one binary very often holds two or three of them at once.

reverse proxy

The doorman.

Defined by position: whatever stands between the internet and your backends. Terminates TLS, sets the forwarding headers, compresses, caches, hides the topology. The most general of the three names.

load balancer

The dispatcher.

One job: pick which healthy instance gets this request. Round-robin, least-connections, consistent hashing, plus health checks to prune the pool. A load balancer is a reverse proxy with opinions about where, not what.

api gateway

The contract desk.

A reverse proxy with product opinions: API keys, per-customer rate limits, token validation, request transforms, usage metering, versioning. The features product teams ask for, bolted onto the same data path — see API gateways.

One nginx is all three more often than not. The server block terminating TLS is the reverse proxy; the upstream block with least_conn is the load balancer; add auth_request, a rate-limit zone, and a path rewrite and you are running a small API gateway. For one team with a handful of services, that is the right amount of infrastructure: three jobs, one config file, one thing to operate and page on.

Split them when ownership splits. The edge tier (platform team, changes monthly, terminates TLS for the whole company) and the gateway (product teams, changes daily, holds per-endpoint policy) want different release cadences and different blast radii — a gateway config mistake should not be able to take down TLS for everything behind it. A separate L4 load balancer in front (NLB, a keepalived pair) earns its keep when the proxy tier itself must be replaceable without touching DNS. Split on ownership and blast radius, not because the vendor categories have three names.


Nginx, HAProxy, and Envoy do the same job differently

Same job, very different reputations.

Each major proxy product makes a specific bet. Pick by which trade-off you want.

nginx

The default.

Imperative config, event-driven, 20+ years old. Excellent docs. Modules for everything. Reload SIGHUP keeps connections; full restart is ~free. Battle-tested past death.

envoy

The control plane.

Dynamic config via xDS API. Designed for service meshes. First-class stats, retries, circuit breaking, gRPC. The data plane behind Istio, Consul, AWS App Mesh.

haproxy

The load balancer.

L4 and L7. Excellent at high RPS with low resource use. Famous for stats and ACL flexibility. Stateful sticky sessions, runtime API for live updates without reloads.

And Caddy, the newcomer: defaults to automatic HTTPS via Let's Encrypt, declarative config, written in Go. Smallest config of any of them. Wonderful for small sites; less common at high RPS. Traefik makes a different bet again — service discovery instead of a config file — which is a big enough fork in the road that we compare Traefik vs Nginx head to head.


How the proxy tells the backend who really called

X-Forwarded-For and friends carry the original client IP.

By convention, a reverse proxy adds X-Forwarded-For (originating client IP), X-Forwarded-Proto (http vs https), X-Forwarded-Host, and increasingly the standardized Forwarded header (RFC 7239). Backends rely on these to know who the real caller is — the TCP source they see is the proxy itself.

The hazard: if the backend is reachable directly, an attacker can spoof these headers. Always strip and re-emit them at the proxy. Trust the proxy, not the request.


Two ways a proxy checks a backend is healthy

Active probing, or passively watching for failures.

Active health checks: the proxy probes each backend on an interval, marking it unhealthy after N failures. Cheap; explicit. The downside: probe traffic adds load.

Passive health checks: the proxy notes connection failures or 5xx responses and quarantines the backend (much like a circuit breaker). No extra traffic; reactive.

For graceful shutdown, the backend should stop accepting new connections, finish in-flight requests, then exit. The proxy needs drain support — pause sending new requests to a backend without yanking existing ones. Without this, every deploy drops a few requests.


Where reverse proxies sit: edge, cluster, or sidecar

Edge, cluster, sidecar.

Edge proxy: one big tier in front of everything, terminates TLS for the public internet. AWS ALB, Cloudflare, your Nginx fleet.

Cluster proxy: per-cluster ingress (Kubernetes Ingress controller, Istio gateway). Routes external traffic into the cluster.

Sidecar proxy: one per workload, transparent to the app. Service mesh data plane (Envoy in Istio, Linkerd, Consul Connect). Does mTLS, retries, telemetry without app code — see service discovery.

Big shops run all three. Each layer adds cost; each adds capability. Keep the model deliberate or it becomes nesting dolls.


NGINX vs HAProxy vs Envoy: choose by use case, not benchmark

The big three, side by side.

NGINX · 2004
Originally a web server with reverse-proxy features bolted on; today the most-deployed reverse proxy on the web (~33% of all sites per W3Techs 2024). Configuration is the famous nginx.conf directive language. Good at static-file serving, simple HTTP routing, basic load balancing. Hot-reload requires re-execing; configuration changes that need new connections drop in-flight requests. Free tier; NGINX Plus adds metrics and active health checks.
HAProxy · 2001
The oldest of the three, born for L4/L7 load balancing. Famous for raw throughput — 100k+ requests per second per core, predictable latency, very low memory footprint (~50 MB for thousands of backends). Configuration is the haproxy.cfg file. Used at scale by Stack Overflow, Reddit, Imgur, GitHub for the front edge. Not as friendly for service mesh patterns; runs as a process, not a control plane.
Envoy · 2016
Born at Lyft, donated to CNCF, the data plane behind Istio, Linkerd, AWS App Mesh. Configuration is API-driven (xDS protocol) — designed for control planes that push updates dynamically, no reload required. Higher memory footprint than HAProxy (~150 MB baseline), more CPU per connection — but the dynamic-configuration story is incomparably better. The default in modern service-mesh deployments.

Picking by use case. Static-file serving + simple TLS termination: NGINX. Edge L4/L7 at very high throughput with stable backend pool: HAProxy. Service mesh, dynamic backends, gRPC, fine-grained traffic shifting: Envoy. The performance differences in modern benchmarks are within ~20% of each other — pick by configuration model, not microbenchmarks.


Three reverse-proxy stacks running at scale

How the big sites configure them.

Cloudflare — NGINX-customised at the edge. Cloudflare's edge fleet ran a deeply forked NGINX (~1,000 patches) for years; in 2022 they began migrating to "Pingora", a Rust-native reverse proxy they built specifically for their workload (~1 trillion requests/day). The published numbers: ~70% reduction in CPU per request, ~67% reduction in memory. Pingora handles ~40 million requests per second across the network.

Stripe — Envoy as the API gateway. Stripe terminated all incoming API requests at an Envoy fleet starting around 2019. The motivation: dynamic configuration. Envoy lets Stripe shift traffic between API versions, route certain customers to canary backends, and apply per-endpoint rate limits — all via xDS push without reload.

GitHub — HAProxy for raw throughput. GitHub's edge has been HAProxy for the public web traffic for over a decade. Their public engineering posts emphasise the operational simplicity: a single binary, a single config file, hot-reload via dual sockets. At ~80,000 requests per second per edge node, HAProxy's throughput-per-CPU is hard to beat.



A closing note

Reverse proxies are the one piece of infrastructure that almost everyone ends up touching. Read the config from the top — listener, then routes, then upstream — and most of the mystery dissolves into "five known jobs, applied in order".

Found this useful?