A middleman for network traffic. Which direction it faces determines whether it’s protecting your users or your servers.
Prerequisites: Networking 101, Load Balancers Time to read: ~12 minutes
Client talks to server. Simple. But sometimes you want something in the middle — to cache, to filter, to hide who’s talking, to add security, to route.
A proxy is that middle thing. The only question is which side it works for, and that single distinction is what interviewers are checking when they ask.
Sits in front of clients. The server has no idea who the real client is.
[Client] → [Forward Proxy] → Internet → [Server]
↑
acts on behalf of the client;
the server sees the proxy's IP
What it’s for:
In system design terms: you’ll mostly meet forward proxies as a constraint rather than a component you design. “Our enterprise customers are behind a corporate proxy” affects TLS, IP allowlists, and whether WebSockets work at all.
Examples: Squid, corporate web gateways, Zscaler, and a NAT gateway (which is the same idea at the network layer — your VPC’s private instances reach the internet through it).
Sits in front of servers. The client has no idea which backend served it.
[Client] → Internet → [Reverse Proxy] → [Server 1]
│ → [Server 2]
↓ → [Server 3]
acts on behalf of the servers;
the client sees only the proxy
This is the one you’ll actually design with. Nearly every production system has one, and it’s doing more than you think:
| Function | Detail |
|---|---|
| Load balancing | → Load Balancers — an L7 load balancer is a reverse proxy |
| TLS termination | Handle HTTPS once, centrally; backends speak plain HTTP inside the VPC |
| Caching | Serve repeat requests without touching the app at all |
| Compression | gzip/brotli once at the edge instead of in every service |
| Security | Hide backend topology; block bad requests before they reach your code; WAF rules |
| Rate limiting | Cheap rejection before you spend application CPU |
| Static file serving | Nginx serves files far faster than your app framework |
| Request routing | /api/* → API servers, /static/* → the CDN, /ws → the WebSocket tier |
| Header manipulation | Add X-Forwarded-For, X-Request-ID, security headers |
| Canary routing | Send 5% of traffic to the new version |
🚨 The distinction in one line: a forward proxy hides the client from the server; a reverse proxy hides the server from the client. If you can say that sentence, you’ve answered the question.
Examples: Nginx, HAProxy, Envoy, Traefik, Caddy, AWS ALB, Cloudflare.
These terms overlap heavily and beginners find it confusing. The honest answer:
| Component | It is… | What distinguishes it |
|---|---|---|
| Reverse proxy | The general category | Anything sitting in front of servers |
| Load balancer | A reverse proxy | Emphasis on distribution + health checking |
| API gateway | A reverse proxy | Emphasis on auth, rate limiting, aggregation, per-API policy → API Gateway |
| CDN | A globally distributed reverse proxy | Emphasis on geographic caching → CDN |
| Service mesh sidecar | A reverse and forward proxy per service | Emphasis on service-to-service mTLS, retries, telemetry → Service Mesh |
They’re the same underlying software category (Nginx and Envoy can play every one of these roles), differentiated by where you put it and what you configure it to emphasize. Saying that in an interview is better than pretending they’re fundamentally different things.
Once traffic goes through a proxy, your application sees the proxy’s IP on every request. That breaks rate limiting, geo-logic, fraud detection, and audit logs.
The fix is the X-Forwarded-For header (or the standardized Forwarded header):
X-Forwarded-For: 203.0.113.5, 198.51.100.17
↑ original client ↑ first proxy
🚨 Security trap: X-Forwarded-For is client-supplied and trivially spoofable. If you rate limit
by the leftmost value, an attacker sends a fake one and bypasses your limits entirely. The correct
approach is to configure how many trusted proxies sit in front of you, and only trust that many
entries from the right. Every serious framework has a trusted_proxies setting; use it.
Reverse proxies buffer by default. That’s usually good (a slow client can’t tie up a backend worker — this is the defence against slowloris attacks), but it breaks streaming responses and server-sent events unless you disable it for those routes.
And every proxy has its own timeouts, which must be longer than your application’s. A classic
confusing bug: Nginx’s default 60-second proxy_read_timeout returning a 504 while your application
happily completes the work at 70 seconds. The user sees an error; your logs show success.
Rule: timeouts should decrease as you move inward. Client 30s → proxy 25s → service 20s → database 15s. Anything else produces work that nobody is waiting for. → Retries & Timeouts
WebSockets start as an HTTP request with an Upgrade header. Proxies that don’t forward that header
(and don’t hold the connection open) silently break real-time features. If a design involves
WebSockets, the proxy layer needs proxy_http_version 1.1 plus Upgrade/Connection headers, and
long idle timeouts.
| Adding a reverse proxy | Gain | Cost |
|---|---|---|
| Any proxy hop | Centralized TLS, caching, routing, security | +0.1–1 ms latency; another component to run and monitor |
| TLS termination at the proxy | Backends don’t handle certs or crypto | Internal traffic is plaintext unless re-encrypted |
| Caching at the proxy | Big origin load reduction | Invalidation complexity; risk of serving private data if Vary is wrong |
| Buffering | Slow clients can’t tie up backends | Breaks streaming/SSE unless disabled per route |
X-Forwarded-For blindly when rate limiting or geo-blocking.X-Forwarded-For carefully — configure the number of proxies in front and
only read that far in, otherwise clients can spoof their IP and bypass rate limits.”Configure Nginx as a reverse proxy with caching, and watch the origin go quiet:
proxy_cache_path /tmp/cache levels=1:2 keys_zone=my_cache:10m max_size=1g;
server {
listen 8080;
location / {
proxy_pass http://localhost:3000;
proxy_cache my_cache;
proxy_cache_valid 200 60s;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header X-Cache-Status $upstream_cache_status; # HIT / MISS
}
}
Hit it repeatedly and watch X-Cache-Status flip from MISS to HIT. Then check your application
logs — the requests simply stop arriving. That’s the origin-load reduction a proxy buys you, and
it’s larger than most people expect.
Then try spoofing: curl -H "X-Forwarded-For: 1.2.3.4" localhost:8080 and see what your app logs.
That’s the security trap, demonstrated in one command.