Nginx: Reverse Proxy and Load Balancer Configuration
A reverse proxy sits between clients and backend servers, accepting requests on behalf of those servers. Unlike a forward proxy that serves clients by forwarding their requests to various servers, a...
Key Insights
- Nginx’s event-driven architecture handles tens of thousands of concurrent connections with minimal memory footprint, making it ideal for reverse proxy and load balancing compared to thread-based alternatives like Apache
- Choose load balancing algorithms based on application characteristics: round-robin for stateless apps, IP hash for session persistence, least connections for variable request processing times
- Always implement health checks with proper fail_timeout and max_fails parameters to prevent routing traffic to degraded backends—passive checks alone aren’t sufficient for production environments
Introduction to Nginx as Reverse Proxy
A reverse proxy sits between clients and backend servers, accepting requests on behalf of those servers. Unlike a forward proxy that serves clients by forwarding their requests to various servers, a reverse proxy serves servers by distributing client requests among them.
Nginx dominates the reverse proxy space because of its event-driven, asynchronous architecture. Where traditional servers spawn a thread per connection, Nginx uses a small number of worker processes that handle thousands of connections each through non-blocking I/O. This translates to predictable memory usage and exceptional performance under load.
Here’s the simplest possible reverse proxy configuration:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
}
}
This forwards all requests for example.com to a backend application running on port 3000. It works, but it’s missing critical headers and configuration that production systems need.
Setting Up a Basic Reverse Proxy
A production-ready reverse proxy configuration requires proper headers to preserve client information and tuned timeouts to handle slow backends gracefully.
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
# Preserve client information
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;
# Timeout configuration
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer configuration
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
}
The X-Forwarded-For header is crucial—it preserves the original client IP through the proxy chain. Without it, your backend only sees Nginx’s IP address. The X-Forwarded-Proto header tells the backend whether the original request used HTTP or HTTPS, essential for applications that generate absolute URLs.
Timeout settings prevent hung connections from consuming resources. The defaults are often too conservative for modern applications. Adjust proxy_read_timeout based on your longest legitimate request duration.
Buffering improves performance by allowing Nginx to free up backend connections faster. Nginx buffers the response and sends it to slow clients asynchronously, so your application server isn’t waiting on network I/O.
Load Balancing Strategies
Load balancing distributes traffic across multiple backend servers. Define backends in an upstream block, then reference it in proxy_pass.
Round-robin (default):
upstream backend {
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
Round-robin distributes requests sequentially. It’s simple and works well for stateless applications where all backends have similar capacity.
Least connections:
upstream backend {
least_conn;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000;
}
Use least_conn when request processing time varies significantly. Nginx routes new requests to the server with the fewest active connections, preventing overload on servers handling slow requests.
IP hash for session persistence:
upstream backend {
ip_hash;
server 10.0.1.10:3000;
server 10.0.1.11:3000;
server 10.0.1.12:3000;
}
The ip_hash directive ensures requests from the same client IP always go to the same backend. This provides session persistence for stateful applications without shared session storage. The downside: uneven distribution if clients are behind NAT.
Weighted distribution:
upstream backend {
server 10.0.1.10:3000 weight=3;
server 10.0.1.11:3000 weight=2;
server 10.0.1.12:3000 weight=1;
}
Weights let you send more traffic to more powerful servers. With these weights, the first server receives 3x the traffic of the third server.
Health Checks and Failover
Nginx performs passive health checks by default—it marks servers as failed after unsuccessful connection attempts. Configure this behavior explicitly:
upstream backend {
server 10.0.1.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.1.11:3000 max_fails=3 fail_timeout=30s;
server 10.0.1.12:3000 max_fails=3 fail_timeout=30s backup;
}
After max_fails consecutive failures, Nginx stops sending traffic to that server for fail_timeout seconds. Then it tries again. The backup directive marks a server to only receive traffic when all primary servers are down.
This configuration tolerates transient failures without immediately removing servers from rotation. Adjust max_fails and fail_timeout based on your application’s failure patterns and recovery time.
Note that Nginx Open Source doesn’t include active health checks (periodic probing of backends). That feature requires Nginx Plus. For critical systems without Nginx Plus, implement health check endpoints and monitor them externally, removing failed servers from the upstream block through configuration management.
Advanced Configuration
SSL/TLS Termination:
Terminate SSL at the reverse proxy to offload encryption overhead from application servers:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://backend;
proxy_set_header X-Forwarded-Proto https;
}
}
Proxy Caching:
Cache backend responses to reduce load and improve response times:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
server {
location / {
proxy_cache my_cache;
proxy_cache_valid 200 60m;
proxy_cache_valid 404 10m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_cache_bypass $http_cache_control;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
}
The proxy_cache_use_stale directive serves cached content even when backends are down or slow—critical for availability.
Rate Limiting:
Protect backends from abuse:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
}
This allows 10 requests per second per IP, with bursts up to 20 requests.
Monitoring and Troubleshooting
Enable the stub_status module for basic metrics:
server {
listen 8080;
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
Access it with curl localhost:8080/nginx_status to see active connections, requests per second, and connection states.
Configure detailed logging for troubleshooting:
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'upstream: $upstream_addr '
'upstream_status: $upstream_status '
'request_time: $request_time '
'upstream_response_time: $upstream_response_time';
access_log /var/log/nginx/access.log detailed;
The $upstream_response_time variable is invaluable for identifying slow backends.
Production Best Practices
Here’s a complete production configuration incorporating security, performance, and reliability:
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
}
http {
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Upstream configuration
upstream backend {
least_conn;
server 10.0.1.10:3000 max_fails=3 fail_timeout=30s;
server 10.0.1.11:3000 max_fails=3 fail_timeout=30s;
server 10.0.1.12:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
limit_req zone=general burst=20 nodelay;
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
}
Set worker_processes to auto to match CPU cores. The keepalive directive in the upstream block maintains persistent connections to backends, reducing latency. The proxy_http_version 1.1 and Connection "" headers enable HTTP keepalive to backends.
Test configuration changes with nginx -t before reloading. Monitor error logs closely after deployment. With proper configuration, Nginx handles hundreds of thousands of requests per second on modest hardware while providing the reliability and flexibility production systems demand.