Skip to content
SeeItFlow
Deep dive · 9 chapters · ~26 min read

Reverse Proxy · Production Engineering

How a reverse proxy actually does its jobs in production: the internals of forwarding, SSL termination, routing, static handling, caching, security, and scaling — plus a full failure-and-scale playbook for when each of those pieces breaks under real traffic.

Mangesh MandlikLast updated:
01 · Internals

Request Forwarding Internals

Forwarding is not a redirect. The proxy maintains two independent connections and copies bytes between them, which is what makes every other feature possible.

Swipe to pan
ClientReverse ProxyBackend1 · request2 · forward3 · response4 · relay
Two independent connections — client↔proxy and proxy↔backend — with the proxy buffering and relaying between them.

When a request arrives, the proxy accepts the client connection, parses the request line and headers, and decides where it goes. It then borrows a connection to the chosen backend from a keep-alive pool (or opens a new one), rewrites hop-by-hop headers, adds forwarding headers like X-Forwarded-For and X-Forwarded-Proto, and streams the request upstream.

On the way back it buffers the response so a slow client can't tie up a precious backend worker — the backend finishes fast and moves on, while the proxy dribbles bytes to the client at whatever rate it can accept. Connection pooling, buffering, and header rewriting are the unglamorous mechanics that let a small backend serve many slow clients.

nginxUpstream pool with keep-alive connection reuse.
upstream app_servers {
  server 10.0.0.11:8080;
  server 10.0.0.12:8080;
  keepalive 32;            # reuse backend connections
}

location / {
  proxy_pass http://app_servers;
  proxy_http_version 1.1;
  proxy_set_header Connection "";
}
02 · TLS

SSL Termination Flow

Terminating TLS at the proxy centralises the most error-prone public-edge concern in a web service: certificates and handshake policy.

Swipe to pan
Before · cert per backendAfter · terminate onceClient🔒 App 1🔒 App 2🔒 App 3Client🔒 ProxyAppApphttpsplain http
The public TLS handshake completes at the proxy; the proxy→backend hop can be plain HTTP on a trusted network or a separate HTTPS/mTLS connection.

The client and proxy perform the TLS handshake — certificate presentation, key exchange, cipher negotiation, and ALPN if needed. Once that finishes, the proxy holds the decrypted HTTP request in memory and can route, cache, compress, inspect, or reject it before any backend sees it. That L7 visibility is exactly why TLS usually terminates here rather than at a blind L4 hop.

What happens next is a separate design choice. In the common termination pattern, the proxy forwards plain HTTP over a trusted private network. In stricter environments it opens a new HTTPS or mTLS connection upstream, so public TLS still terminates at the proxy but internal traffic stays encrypted too. If you instead use TCP/TLS pass-through, the backend keeps the certificate and the proxy loses most HTTP-aware features.

Because the certificate and private key live only on the public proxy tier, rotation is a single automated job (for example, ACME/Let's Encrypt renewals), TLS version and cipher policy is one config, and OCSP stapling and session resumption are configured once. Backends either avoid public crypto material entirely or only handle the internal trust relationship for re-encryption.

03 · Routing

Routing Engine

The routing engine turns a request's host and path into a backend decision, and the order of rules is everything.

Swipe to pan
ClientReverse Proxyroute by pathAPI/apiAdmin/adminStatic/staticGET /api/orders
Host + path matched against an ordered set of location rules, each pointing at an upstream.

Nginx matches location blocks by a defined precedence: an exact (=) match ends the search immediately; otherwise nginx remembers the longest matching prefix location, then checks regex locations in file order and uses the first regex that matches — unless the remembered prefix has the ^~ modifier, in which case regexes are skipped. If no regex matches, the remembered longest prefix is used. A request is resolved to exactly one location, which selects an upstream. A broad / prefix does not shadow a specific /api prefix, but a regex location can override a prefix location, silently sending API traffic to the wrong place.

Beyond static paths, the engine can route on headers, query args, and rewritten URIs, and it can strip or preserve prefixes before forwarding. The discipline is to keep rules explicit and ordered, and to test them, because routing bugs are invisible until a user lands on the wrong backend.

04 · Static

Static File Handling

Serving static files from the proxy is the cheapest performance win available, but only if you get caching headers and file access right.

Swipe to pan
Clientlogo·js·cssReverse Proxyserves staticApp Serveridle for staticGET /logo.pngserved from diskbypassed
Static requests resolved from the proxy's filesystem; dynamic requests forwarded to the app.

For a static request the proxy maps the URL to a file on disk and streams it with sendfile, letting the kernel copy bytes straight from the file cache to the socket with zero application involvement. Correct Cache-Control/Expires headers then let browsers and CDNs cache the file too, so most requests never even reach the proxy again.

The two operational concerns are freshness and safety. Fingerprinted filenames (app.4f2c.js) let you set long TTLs without stale-content risk. And the file root must be locked down so path traversal can't escape the intended directory.

05 · Caching

Reverse Proxy Caching

Proxy caching trades a little staleness for a lot of backend relief — the engineering is all in the cache key and the TTL.

Swipe to pan
Visitor AmissVisitor BhitProxycachecache · TTL 60sApp Servermiss → backendhit → served
Miss populates the cache from the backend; hit is served from the proxy store while fresh.

The proxy builds a cache key (typically method + host + URI, sometimes plus selected headers), and on a hit within the TTL it returns the stored response without touching the backend. On a miss it fetches, stores, and serves. Request collapsing (in nginx, proxy_cache_lock on, which is off by default) ensures that when a cached item expires, a thundering herd of simultaneous requests results in one backend fetch, not thousands. To keep serving the stale copy to everyone else while that single refresh runs, add proxy_cache_use_stale updating (typically with proxy_cache_background_update on).

Tuning is about correctness first: never cache per-user or authenticated responses under a shared key, honour Cache-Control from the backend, and use short TTLs with stale-while-revalidate for content that changes. Measure hit ratio continuously — it is the single number that tells you whether the cache is earning its keep.

06 · Security

Security Controls

The proxy is your perimeter. Everything you enforce here is enforced for every backend at once.

Standard controls layer up at the edge: TLS with modern ciphers, security headers (HSTS, CSP, X-Frame-Options), IP allow/deny lists, connection and request rate limiting, request size and timeout limits, and optionally a WAF module to block known exploit patterns. Because backends have no public address, all of this is enforced before a hostile request can reach an application.

Rate limiting deserves special attention: leaky-bucket or token-bucket limits per IP or per key blunt brute-force and scraping, and connection limits stop a single client exhausting worker slots. The proxy is ideal for coarse edge protection because it sees every connection. If you also run an API gateway, that gateway may still apply finer-grained limits per consumer, token, or product plan deeper in the stack.

07 · Many services

Multi-Service Routing

One domain, many services: the proxy composes independent backends into a single coherent site.

Swipe to pan
ClientReverse Proxyroute by pathAPI/apiAdmin/adminStatic/staticGET /api/orders
/api, /admin, and /static resolved to three independent backends behind one address.

Each service gets its own upstream pool and its own location rules, so teams deploy and scale independently while presenting one URL space to users. The proxy can also do canary and blue-green routing — sending 5% of /api traffic to a new version by weighting upstreams — and can rewrite paths so a backend's internal routes differ from the public ones.

The trade-off is that the proxy config becomes a shared, high-stakes artifact. Keep it in version control, review changes, and validate (nginx -t) before every reload, because one bad route affects the whole site.

08 · Scaling

Scaling Strategies

The proxy tier scales horizontally and independently — but only if you design it to be stateless and front it with a load balancer.

Swipe to pan
InternetLBhealth-checkedProxy 1nginxProxy 2nginxProxy 3✗ failedApp 1App 2no single point of failure
A load balancer spreads traffic across a cluster of identical, stateless proxy instances.

Run several identical proxy instances behind a dedicated load balancer so no single proxy is a point of failure and so you can add capacity by adding instances. Keep proxies stateless: push shared cache to a distributed tier if needed, and keep TLS session state resumable. Health checks let the load balancer eject a sick proxy automatically.

Scale the layers on their own axes — proxies for connection and edge-work capacity, backends for compute. Because proxies absorb static, cached, and blocked traffic, the backend tier you have to scale is only the genuinely dynamic remainder.

Key takeaways
  1. 01Multiple stateless proxies behind a load balancer = no single point of failure.
  2. 02Scale proxy and backend tiers independently.
  3. 03Offloading static + cached + blocked traffic shrinks the backend you must scale.
09 · When it breaks

Failure & Scale Scenarios

For each failure mode: what breaks, the user impact, how you detect it, how you recover, and the scaling strategy that prevents it next time.

Reverse Proxy Failure

What breaks: a single proxy dies and takes SSL, routing, and cache with it. Impact: total outage for traffic on that instance. Detection: load-balancer health checks fail, 5xx spike, synthetic probes red. Recovery: LB ejects the dead instance, traffic shifts to healthy peers; auto-restart or replace. Scaling: never run one proxy — a cluster of ≥2 behind a load balancer removes the proxy tier as a single point of failure.

Traffic Spike

What breaks: a sudden surge exhausts proxy worker connections and backend capacity. Impact: rising latency, queueing, timeouts. Detection: connection count, request rate, and latency percentiles climb; queue depth grows. Recovery: autoscale proxy and backend tiers, shed non-critical load, lean on cache and static offload. Scaling: pre-provision headroom, enable autoscaling, and raise cache hit ratio so surges hit the edge, not the origin.

Cache Failure

What breaks: the cache tier goes down or is flushed, so every request becomes a miss. Impact: a thundering herd hammers backends; latency spikes. Detection: hit ratio collapses, backend QPS jumps, origin latency rises. Recovery: request collapsing/locking so one fetch repopulates each key; warm critical entries; degrade gracefully to origin. Scaling: replicate the cache, use consistent hashing, and enable stale-while-revalidate to serve stale during repopulation.

SSL Certificate Expiry

What breaks: the cert expires and clients refuse the TLS handshake. Impact: every HTTPS request fails with browser security errors — a hard, total outage. Detection: expiry-monitoring alerts days ahead; handshake-failure and cert-age metrics. Recovery: renew and hot-reload the cert (no restart needed). Scaling: automate renewal (ACME) and alert well before expiry — centralising certs on the proxy makes this one job, not one per server.

Misconfigured Routing

What breaks: a bad location/upstream rule sends traffic to the wrong or a dead backend. Impact: users hit 404/502 or the wrong app; can be partial and sneaky. Detection: per-route error rates and traffic anomalies; failed config validation. Recovery: roll back the config and reload; nginx -t before every apply. Scaling: version-control config, require review, canary config changes, and validate in CI before rollout.

Backend Failure

What breaks: an upstream app instance becomes unhealthy or slow. Impact: errors or latency for requests routed to it. Detection: active/passive health checks fail, upstream 5xx and timeout metrics rise. Recovery: proxy marks it down and stops routing to it (passive ejection), retries idempotent requests on healthy peers. Scaling: run multiple backend replicas, configure health checks and retries, and set sane timeouts so one slow backend can't stall workers.

DDoS Traffic

What breaks: a flood of malicious requests aims to exhaust proxy and backend resources. Impact: legitimate users are crowded out; potential outage. Detection: abnormal request rate from few sources, connection-table saturation, geo/pattern anomalies. Recovery: rate-limit and connection-limit at the edge, block offending IPs/patterns, engage upstream DDoS scrubbing/CDN. Scaling: front the proxy with a CDN/scrubbing layer, keep generous edge capacity, and enforce per-IP limits so the perimeter absorbs the flood.