Nginx Reverse Proxy in Production: Upstream, Caching and Rate Limiting
An Nginx reverse proxy is the most common traffic entry point in production: it forwards external requests to backend application servers and relays responses back to clients. Per the official docs, proxying is typically used to distribute load among several servers or to pass requests to application servers over protocols other than HTTP. Compared with basic Nginx site configuration, a reverse proxy emphasizes the "forwarding" layer — the front line of traffic governance.
1. Upstream: Defining a Backend Server Group
The upstream block defines a group of backend servers; when proxy_pass points to that group, Nginx distributes requests among them using a load-balancing algorithm.
upstream backend_app {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003 weight=2;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend_app;
}
}
The default algorithm is round-robin. Tune it with weight — above, 3003 with weight=2 takes roughly twice the requests. For apps that need session stickiness, ip_hash keeps a given client on the same backend; when backends differ in capacity, least_conn often beats round-robin. If the address in proxy_pass carries no URI, Nginx passes the full original request URI through — the most common usage, so avoid casually appending a /.
2. Forwarding Request Headers
By default Nginx rewrites the Host header to $proxy_host, so the backend no longer sees the real hostname. To preserve it along with client information, set headers explicitly:
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://backend_app;
}
X-Forwarded-For is the foundation of log auditing and security analysis: without it the backend only sees Nginx's own IP, so real clients, rate limits, blocking and attribution all become meaningless. Note that $proxy_add_x_forwarded_for appends the current connection address to any existing value; in multi-proxy setups every layer should append the same way to keep the full chain.
3. proxy_cache: Reverse Proxy Caching
Nginx can cache content at the reverse proxy layer, keeping hot responses in memory and on disk and significantly relieving backend load. Define a cache path with proxy_cache_path and enable it in a location:
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=mycache:10m max_size=1g
inactive=60m use_temp_path=off;
location / {
proxy_cache mycache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend_app;
}
$upstream_cache_status returns HIT/MISS/BYPASS/EXPIRED, and the X-Cache-Status response header lets you quickly verify whether caching works and what your hit rate is. Think hard about boundaries before enabling it: cache only idempotent GET requests. Endpoints involving login state, carts, or personalization need an explicit proxy_cache_bypass or should not be cached at all. For finer-grained cache keys, see the cache key strategy.
4. limit_req: Rate Limiting Against Abuse
limit_req_zone defines the rate and limit_req applies it in a location. A typical anti-abuse configuration:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend_app;
}
rate=10r/s allows an average of 10 requests per second; burst=20 lets up to 20 extra requests queue during a spike, while nodelay passes those burst requests immediately but still counts them. Together they avoid punishing normal traffic peaks while keeping scripted scraping at bay. To standardize the rejection, add limit_req_status 429; so limited clients get a conventional 429 response.
5. TLS Termination
The reverse proxy layer is the ideal place to terminate TLS. Certificates can be issued and renewed automatically with Let's Encrypt and Certbot (or the one-click certbot script):
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://backend_app;
}
}
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
After terminating TLS at Nginx, internal backends can communicate over plaintext HTTP, and certificates are managed in one central place, cutting internal certificate overhead. X-Forwarded-Proto matters for the backend to know whether a request arrived over HTTPS — otherwise it may build wrong callback URLs. For a complete TLS baseline see CDN SSL/TLS configuration best practices; if you have enabled HTTP/3, check the HTTP/3 acceleration guide.
6. Buffering and Timeouts
Production setups usually tune proxy buffering so slow clients do not drag down the backend:
location / {
proxy_buffering on;
proxy_buffers 16 4k;
proxy_buffer_size 8k;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_pass http://backend_app;
}
By default Nginx buffers backend responses and forwards them only after receiving them fully — a necessary optimization for slow clients. But for streaming endpoints (SSE, real-time pushes), turn buffering off so the first byte reaches the client quickly. Set timeouts in layers: proxy_connect_timeout is for establishing the connection, while proxy_read_timeout is the interval between two read operations — not the total request duration.
7. Working with Containers
When backends run inside Docker Compose, Nginx usually acts as the host-level entry proxy to container ports; see Docker Compose production deployment. For deeper Nginx performance tuning see the complete Nginx optimization guide, and for LNMP integration see the LNMP stack setup.
Common troubleshooting scenarios
Three recurring "forwarding anomalies" can usually be pinned down quickly at the Nginx layer:
- Backend logs show only 127.0.0.1 as the client IP: almost certainly
X-Forwarded-Foris missing, so the backend only sees the proxy address. - The cache does not seem to work: run
curl -Iand check theX-Cache-Statusheader; if it is always MISS, verify whetherproxy_cache_keydiffers per request because it includes$args. - 504 Gateway Timeout: usually
proxy_read_timeoutis too short and the backend takes longer than the limit; raise the timeout or optimize the backend query.
16IDC Note
A reverse proxy is more than "just forwarding": it is the front line of traffic governance. Load balancing lets multiple instances share the load, caching keeps hot requests away from the backend, rate limiting blocks abnormal traffic, and TLS termination centralizes certificate management. Handle these four things well at the Nginx layer and the backend can focus on business logic, making the whole architecture noticeably more robust.
Source: https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/
Reference: Nginx content caching guide https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/