Proxy vs reverse proxy
Proxy vs reverse proxy for system design interviews: forward proxies for egress control, reverse proxies for TLS termination, routing, caching and buffering, X-Forwarded-For and trusting client IPs, sidecars and service mesh, and the 502 vs 504 failure modes.
On this page
- Picture it first: same box, opposite sides
- The forward proxy: controlling what leaves
- The reverse proxy: the front door of almost every design
- What the app sees: X-Forwarded-For and Forwarded
- Sidecars and service mesh: a proxy next to every service
- Reverse proxy vs load balancer vs API gateway vs CDN
- A minimal reverse proxy config, with timeouts
- Failure modes: what breaks and why
- How it shows up in design problems
- Interview follow-ups
- How to say it in the interview
🪞 What is a proxy, and what makes one "reverse"?
A proxy is a server that sits in the middle of a connection and makes requests on someone else's behalf. A forward proxy acts for a group of clients: your office laptop talks to the proxy, and the proxy talks to the internet. A reverse proxy acts for a group of servers: the internet talks to api.example.com, which is really a proxy that passes each request to one of the app servers hidden behind it. Think of a forward proxy as a personal assistant who places calls for you, and a reverse proxy as a company switchboard that answers calls for everyone in the building.
For this lesson, we'll focus on what each kind does, how it changes what your app sees (client IPs especially), and the failures that show up as 502s and 504s. Interviewers like it because nearly every design has a reverse proxy in it, often under another name, and they want to know you understand what that box does.
Below the line (out of scope)
- Spreading load across many servers, and L4 vs L7: Load balancing
- Auth, quotas and API products at the edge: API gateway
- Caching static content close to users: Content delivery network (CDN)
- Encrypting service-to-service traffic: Service-to-service auth and mTLS
The 30-second answer
Both are middlemen; the difference is whose side they're on. A forward proxy sits in front of clients and represents them to the internet, for egress control, allow-lists, auditing and hiding the client's address. A reverse proxy sits in front of servers and represents them to clients: it terminates TLS, routes by host and path, caches and compresses responses, buffers slow clients, hides the origin servers and can run a WAF. Because the proxy opens a new connection to the backend, the app sees the proxy's IP, so the real client IP travels in X-Forwarded-For or the standard Forwarded header, and you only trust it from proxies you control. The proxy must be redundant, its timeouts must line up with the layers around it, and a 502 means the upstream gave a bad or no response while a 504 means it was too slow.
What you’ll learn
- Tell a forward proxy (acts for clients) from a reverse proxy (acts for servers) and give a design use for each
- List what a reverse proxy does: TLS termination, routing, caching, compression, buffering, hiding origins, WAF
- Recover the real client IP from
X-Forwarded-FororForwardedwithout trusting spoofed entries - Place reverse proxies, load balancers, API gateways, CDNs and sidecars correctly in a design
- Set proxy timeouts that line up with the layers around them, and read a 502 vs 504 correctly
01Picture it first: same box, opposite sides
The software can be identical (nginx, Envoy and HAProxy can play either role). What changes is which side it guards. Here are both, side by side.
Forward proxy vs reverse proxy
Notice three things:
- The client chooses a forward proxy. Laptops are configured (or forced by the network) to send traffic to it. Websites never asked for it.
- The server owner chooses a reverse proxy. Clients have no idea it exists; DNS for
api.example.compoints at it. - Both break the end-to-end connection. There are two TCP connections, client to proxy and proxy to server, which is exactly why the backend loses the client's IP.
| Forward proxy | Reverse proxy | |
|---|---|---|
| Acts for | Clients | Servers |
| Set up by | The client's organisation | The service owner |
| Typical jobs | Egress allow-lists, auditing, content filtering, shared cache, hiding client IPs | TLS termination, routing, caching, compression, buffering, hiding origins, WAF, rate limiting |
| Who knows it's there | The client | Only the server side |
| Example | Corporate egress proxy, Stripe's Smokescreen | nginx in front of an app, Cloudflare in front of a site |
02The forward proxy: controlling what leaves
Forward proxies show up in interviews less often, but one case is worth knowing cold: your servers calling URLs that someone else chose. Webhooks and "import from URL" features let a user point your backend at an address like http://10.0.0.12:6379/ and probe your private network. That's server-side request forgery (SSRF).
The fix is an egress proxy that every outbound call must use. Stripe open-sourced theirs: Smokescreen carries most of Stripe's outbound traffic, including webhooks, checks hostnames against an allow-list and refuses to connect to internal addresses. The other classic uses:
- Corporate egress: one controlled exit to the internet, with logging and content filtering.
- Allow-lists for partners: a partner whitelists one stable egress IP instead of your whole autoscaling fleet.
- Privacy: the destination sees the proxy's address, not the client's.
03The reverse proxy: the front door of almost every design
A reverse proxy earns its place by doing work once, at the edge, so every app server doesn't have to.
| Job | What it does | Why it matters in a design |
|---|---|---|
| TLS termination | Holds the certificate and decrypts HTTPS | Certificates live in one place; apps speak plain HTTP (or mTLS) on the private network |
| Routing | Picks an upstream by host, path or header | /api/* to one service, /static/* to another, behind one domain |
| Caching | Stores cacheable responses | Repeat reads never reach the app |
| Compression | gzip or Brotli on the way out | Smaller responses without touching app code |
| Request and response buffering | Reads the whole request from a slow client before bothering the app, and absorbs the response so the app is freed quickly | A phone on a weak network ties up a cheap proxy connection, not an app thread |
| Hiding origins | Only the proxy's IP is public | Attackers can't target app servers directly |
| WAF and rate limits | Blocks known attack patterns, throttles abusers | Bad traffic is dropped before it costs you app CPU |
Buffering is the one candidates forget, and it's the most "under the hood" item on the list. nginx buffers both directions by default: proxy_request_buffering and proxy_buffering are both on. With request buffering, the entire body is read from the client before nginx sends anything upstream, so an app worker isn't held for the 20 seconds a mobile client takes to upload a form.
Cloudflare's whole network is a reverse proxy, and in 2022 they wrote about replacing NGINX with Pingora, a proxy they built in Rust that by then served over 1 trillion requests a day using about a third of the CPU and memory of the old setup. The biggest reason was connection reuse to origins: NGINX keeps a separate upstream connection pool per worker process, so more workers meant more scattered pools and more fresh TCP and TLS handshakes. Pingora shares one pool across threads. For one large customer, reuse went from 87.1% to 99.92%, cutting new connections to their origins by 160x. The lesson for interviews: a reverse proxy's value isn't only what it blocks, it's also the warm, reused connections it keeps to your backends.
04What the app sees: X-Forwarded-For and Forwarded
Here's the catch with two TCP connections. The app's socket says the request came from the proxy, so anything that needs the real client (rate limits per IP, geo-blocking, fraud signals, audit logs) reads a header the proxies add. The de facto one is X-Forwarded-For; RFC 7239 standardises the same idea as Forwarded: for=203.0.113.7;proto=https;by=198.51.100.4. Each proxy appends the address it received the connection from, so the list grows left to right.
How X-Forwarded-For grows hop by hop
The rule I use: trust the header only as far as your own proxies wrote it. Start at the right end, skip every address that belongs to a proxy you control, and the first one left is the client. Taking the leftmost entry is the classic bug: an attacker sends X-Forwarded-For: 192.0.2.66 and suddenly every request "comes from" a fresh IP, which walks straight past a per-IP rate limiter.
You rarely parse this by hand. nginx's realip module does it with set_real_ip_from (the CIDRs of your trusted proxies) plus real_ip_recursive on, which picks the last non-trusted address. In Spring Boot, one property makes the embedded server honour the headers, so redirects, request.getRemoteAddr() and generated links use the client's view:
# application.properties: trust X-Forwarded-* / Forwarded from the proxy in front
server.forward-headers-strategy=native
Only turn this on when the app is reachable solely through your proxy. If clients can hit the app directly, they can set the header themselves.
05Sidecars and service mesh: a proxy next to every service
So far the proxy sits at the edge. Inside a microservice system, the same idea moves next to each service. Envoy describes itself as an L7 proxy designed to run alongside every application server: the app talks to localhost, and its local Envoy handles service discovery, load balancing, retries, timeouts, mTLS and metrics on the way out. Put one beside every instance, add a control plane that pushes config to all of them, and you have a service mesh (Istio and similar products use Envoy this way).
The trade-off: the same network behaviour in every language, paid for with an extra local hop on each side of every call and a fleet of proxies to run. A mesh is worth proposing once you have dozens of services in several languages; for five services, a shared client library is simpler. Service discovery covers how sidecars find each other.
06Reverse proxy vs load balancer vs API gateway vs CDN
These four overlap, and one product can play several roles at once. Treat them as one family with different emphasis.
| Box | Main job | Knows about | Lives | Go deeper |
|---|---|---|---|---|
| Reverse proxy | Front door: TLS, routing, buffering, caching, hiding origins | HTTP requests | In front of your servers, in your region | This page |
| Load balancer | Spread traffic across many healthy copies | Connections (L4) or requests (L7), plus health | In front of each pool | Load balancing |
| API gateway | Productise an API: auth, quotas, API keys, request transforms, per-client policy | Who the caller is and what plan they're on | At the public edge of your API | API gateway |
| CDN | Serve and cache content from many cities near users | Cacheability, geography | Hundreds of edge locations worldwide | CDN |
My one-liners: every L7 load balancer is a reverse proxy; an API gateway is a reverse proxy that knows your customers; a CDN is a reverse proxy spread across the planet.
07A minimal reverse proxy config, with timeouts
Try it yourself first
Before reading the config, decide three numbers for an orders API whose p99 is 800 ms: how long the proxy should wait to connect to a backend, how long it should wait for a response, and the largest request body it should accept. Then ask yourself what nginx would use if you set none of them.
Here's a short nginx config for that service. Without these lines, nginx's defaults are 60 seconds for proxy_connect_timeout, proxy_read_timeout and proxy_send_timeout, and a 1 MB client_max_body_size (larger bodies get 413).
upstream orders_api {
server 10.0.1.11:8080;
server 10.0.1.12:8080;
keepalive 32; # reuse warm connections to the apps
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/tls/api.crt;
ssl_certificate_key /etc/nginx/tls/api.key;
client_max_body_size 2m;
location /orders/ {
proxy_pass http://orders_api;
proxy_http_version 1.1;
proxy_set_header Connection ""; # needed for upstream keepalive
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 2s; # a healthy backend accepts in ms
proxy_read_timeout 5s; # ~6x the p99, not 60 s
proxy_send_timeout 5s;
}
}
Notice that proxy_read_timeout isn't a limit on the whole response. nginx documents it as the gap allowed between two successive reads, so a backend that trickles a byte every 4 seconds never trips it. If you need a hard deadline per request, enforce it in the app as well.
08Failure modes: what breaks and why
| Failure | What you see | Why it happens | The fix |
|---|---|---|---|
| Proxy as a SPOF | Everything is down, apps are healthy | One proxy box, or one proxy per zone with no failover | Run several behind an L4 balancer or anycast; see Single point of failure |
| Buffering large uploads | Disk fills, uploads slow, 413s | Request buffering spools multi-GB bodies to proxy disk before the app sees a byte | Turn off proxy_request_buffering for upload routes, or upload straight to object storage with a presigned URL |
| Buffering streams | SSE or token streaming arrives in one lump at the end | Response buffering holds chunks the app already sent | proxy_buffering off (or X-Accel-Buffering: no) on streaming routes |
| Mismatched timeouts | Users see 504s while the app logs success; duplicate work | Outer layer gives up first; the client retries while the first attempt still runs | Make each outer timeout longer than the one inside it, and keep retries at one layer |
| Idle keepalive mismatch | Sporadic 502s under light load | The app closes an idle connection just as the proxy reuses it | App's keepalive idle timeout longer than the proxy's |
The last row is documented by AWS for its Application Load Balancer: if the application's idle timeout is shorter than the load balancer's (60 seconds by default), the load balancer can send a request on a connection the app just closed and return 502 Bad Gateway to the client. It's the most confusing 502 there is, because nothing is actually down.
Where 502 and 504 come from
Both codes come from RFC 9110: 502 Bad Gateway means a proxy got an invalid response from upstream (proxies also use it when the upstream refuses or resets the connection), and 504 Gateway Timeout means no response arrived in time. A 503 usually means nothing healthy was available. So 502 sends me to "is the upstream up and speaking HTTP?", and 504 to "what is the upstream waiting on?".
Many candidates set every timeout in the stack to the same round number, say 30 seconds at the CDN, the load balancer, the proxy and the app's database client. It looks tidy, but it's frequently a mistake: whichever layer's timer started first fires first, the user gets a 504, the app finishes the work anyway, and a client retry runs it twice. Order them instead: the innermost call (database, downstream service) times out first, and each layer outward allows a little more. Fault tolerance covers timeouts and retries properly.
09How it shows up in design problems
- Design a webhook delivery service: outbound calls go through a forward egress proxy that blocks private IPs and gives customers one stable IP range to allow-list.
- Design an LLM gateway: a reverse proxy in front of model providers, with response buffering off so tokens stream, and read timeouts long enough for slow generations.
- Design Dropbox / Google Drive: large uploads shouldn't be buffered through the proxy at all; hand out presigned URLs to object storage.
- Design a rate limiter: the limiter often runs in the reverse proxy, and limiting per IP only works if you extract the client IP from trusted hops.
At this point, you should ask your interviewer whether the API is public or internal. A public one needs TLS, a WAF and real-client-IP handling at the edge; an internal one may only need a sidecar or a load balancer.
10Interview follow-ups
What if the backend needs the client's real IP?
The proxy appends it to X-Forwarded-For or Forwarded, and the app reads it from the right, skipping its own proxies' addresses. For non-HTTP traffic through an L4 balancer, the PROXY protocol prepends the original address to the TCP stream. Either way, the app must not be reachable except through the proxy.
What if TLS is terminated at the proxy: is the traffic behind it unencrypted?
It can be, and inside one private network many teams accept that. If you're handling payments, health data or a zero-trust network, re-encrypt from proxy to app, usually with mTLS so each side proves who it is. The cost is extra handshakes, which keepalive pools mostly hide.
What if users report random 502s but every server looks healthy?
I'd check idle timeouts first: if an app closes idle keepalive connections sooner than the proxy, the proxy occasionally sends a request down a dead connection. Then check deploys, since a backend killed without draining resets in-flight connections. The access log's upstream status and upstream address fields usually tell you which hop failed.
11How to say it in the interview
When I ask about proxies, I'm listening for three things: that you can say whose side each proxy is on, that you know what the reverse proxy box actually does (not "it forwards requests"), and that you know what it breaks, especially client IPs and timeouts.
In the interview
"A forward proxy sits in front of clients and represents them outward; I'd use one for egress control, for example so webhook calls to customer URLs can't reach our internal network. A reverse proxy sits in front of our servers and represents them inward. Ours terminates TLS, routes by path to the right service, compresses and caches what it can, and buffers slow clients so app workers aren't tied up. It also hides the origin servers and is where a WAF and coarse rate limits run. Because it opens a new connection to the backend, the app reads the client IP from X-Forwarded-For, trusting only entries our own proxies added. I'd run at least two proxy instances per zone behind an L4 balancer so it's not a single point of failure. Timeouts go innermost-first: database, then app, then proxy, then client, and I'd turn buffering off for streaming and big uploads. If we see 502s, the upstream gave a bad or no response; 504s mean it was too slow."
On the whiteboard
- Forward proxy: acts for clients → egress, allow-list, SSRF guard
- Reverse proxy: acts for servers → TLS · routing · cache · gzip · buffering · hide origin · WAF
- Client IP: XFF / Forwarded (RFC 7239), read right-to-left, trust own hops only
- Sidecar (Envoy) per service + control plane = service mesh
- L7 LB ⊂ reverse proxy · API gateway = proxy + callers · CDN = proxy worldwide
- Timeouts: inner < outer · app keepalive > proxy idle
- 502 bad/no upstream reply · 504 upstream too slow · 503 nothing healthy
- Redundant: ≥ 2 per zone behind L4 / anycast
Further reading (6)
- RFC 7239: Forwarded HTTP Extension: The standard header for passing the original client address and protocol through a chain of proxies.
- RFC 9110: HTTP Semantics (502 and 504): The precise meaning of Bad Gateway and Gateway Timeout when a server acts as a proxy.
- nginx: ngx_http_proxy_module: Default timeouts, request and response buffering, and how proxy_read_timeout is measured between reads.
- Envoy: What is Envoy: The sidecar model: an out-of-process L7 proxy beside every service, forming a mesh.
- What is a reverse proxy? (Cloudflare Learning Center): A clear walk-through of forward vs reverse proxies and the benefits of each.
- How we built Pingora, the proxy that connects Cloudflare to the Internet (Cloudflare Blog, 2022): Why upstream connection reuse dominated a reverse proxy rewrite at over a trillion requests a day.