devops

Nginx Reverse Proxy: Performance Tuning and Advanced Configuration

Optimize Nginx as a reverse proxy with worker tuning, caching, compression, keepalive, and buffer settings. Production-ready configuration included.

June 29, 2026·7 min read·
#nginx#reverse-proxy#performance#caching#linux

Introduction

Nginx powers over 30% of the world's websites. It's fast out of the box, but the default configuration is conservative--designed to work everywhere, optimized for nowhere.

If you're running Nginx as a reverse proxy in front of Node.js, Python, Go, or PHP backends, tuning a handful of settings can double your throughput and cut latency in half.

This guide covers production-tested Nginx optimizations: worker configuration, buffer tuning, keepalive connections, caching strategies, Gzip compression, and rate limiting. Every setting comes with a concrete before-and-after impact.

Worker Process Tuning

Nginx uses an event-driven architecture with worker processes. Two settings matter most:

worker_processes

Set this to auto on modern Nginx (1.3.8+). It matches your CPU core count:

worker_processes auto;

For containers, you may need to set it explicitly since auto can't always detect cgroup limits:

worker_processes 2;  # Match your container's CPU limit

worker_connections

Each worker can handle this many simultaneous connections. The default (512) is far too low for a reverse proxy:

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}
SettingDefaultRecommendedImpact
worker_connections5124096+Max connections = workers x connections
use epollselectepoll10x+ efficiency on Linux
multi_acceptoffonAccepts all new connections at once

With 4 workers and 4096 connections each, Nginx handles 16,384 concurrent connections. The worker_rlimit_nofile in the main context must support this:

worker_rlimit_nofile 16384;

Buffer Size Tuning

Nginx buffers request and response bodies. Defaults are tiny for modern workloads:

http {
    # Client request body buffer
    client_body_buffer_size 16k;
    client_max_body_size 50m;   # Max upload size
    client_body_timeout 30s;

    # Client header buffer
    client_header_buffer_size 2k;
    large_client_header_buffers 4 16k;

    # Proxy buffers (critical for reverse proxy!)
    proxy_buffer_size 16k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;

    # Response buffering
    proxy_buffering on;
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;
}

Why this matters: If proxy_buffer_size is too small, Nginx writes upstream responses to disk instead of memory--massive latency hit. The proxy_buffers setting (8 x 32k = 256KB total) should comfortably fit most API responses.

Proxy Buffering: When to Disable

For streaming or Server-Sent Events (SSE), disable buffering:

location /stream {
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 86400s;  # Long-lived connection
    proxy_pass http://backend;
}

Keepalive Connections

Upstream Keepalive

Nginx opens a new connection to backends for each request by default. This wastes TCP handshakes:

upstream backend {
    server 10.0.1.10:3000;
    server 10.0.1.11:3000;
    keepalive 64;              # Keep 64 idle connections alive
    keepalive_timeout 60s;
    keepalive_requests 1000;   # Requests per connection before recycling
}

server {
    location / {
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # Required for keepalive!
        proxy_pass http://backend;
    }
}

Impact: A keepalive pool of 64 connections eliminates TCP handshake overhead for most workloads. For services handling 1000+ req/s, this alone can reduce p99 latency by 30-50ms.

Client Keepalive

Improve client-side connection reuse:

http {
    keepalive_timeout 65;
    keepalive_requests 100;
}

65 seconds matches common browser keepalive timeouts. Increase keepalive_requests for API clients that reuse connections heavily.

Proxy Caching

Nginx can cache backend responses, dramatically reducing backend load:

http {
    # Cache path: 100MB, 1 level structure, inactive=60m
    proxy_cache_path /var/cache/nginx levels=1:2
                     keys_zone=my_cache:100m
                     max_size=1g
                     inactive=60m
                     use_temp_path=off;

    proxy_cache_key "$scheme$request_method$host$request_uri";
}

server {
    location /api/ {
        proxy_cache my_cache;
        proxy_cache_valid 200 10m;     # Cache 200 for 10min
        proxy_cache_valid 404 1m;      # Cache 404 for 1min
        proxy_cache_bypass $http_cache_control;  # Honor client no-cache
        proxy_cache_use_stale error timeout updating;
        add_header X-Cache-Status $upstream_cache_status;

        proxy_pass http://backend;
    }
}

Key decisions:

SettingRecommendationWhy
keys_zone size1MB per 8000 keysTracks cache metadata in memory
max_size1-10GBLimit disk usage
inactive60mEvict untouched items
use_temp_path offAlwaysWrites directly to cache, avoids copy
proxy_cache_use_staleupdatingServe stale while refreshing

Verify caching works:

curl -I https://yoursite.com/api/users | grep X-Cache-Status
# X-Cache-Status: MISS (first request)
# X-Cache-Status: HIT  (subsequent requests)

Gzip Compression

Gzip reduces response sizes by 60-80%. Enable it for text-based responses:

http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        image/svg+xml;
    gzip_disable "msie6";
}
SettingValueWhy
comp_level5-6Sweet spot: 75% compression at low CPU cost
min_length256-1000Don't compress tiny responses (overhead > savings)
gzip_proxiedanyCompress proxied responses too

Never gzip already-compressed formats (images, videos, pre-compressed files):

location ~* \.(jpg|jpeg|png|gif|ico|webp|mp4|webm|zip|gz|bz2)$ {
    gzip off;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

Security & Rate Limiting

Rate Limiting

Protect backends from abuse and brute-force attacks:

http {
    # Define a shared memory zone for rate limiting
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

    # Connection limiting
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
}

server {
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        limit_conn conn_limit 10;
        proxy_pass http://backend;
    }

    location /login {
        limit_req zone=login_limit burst=3 nodelay;
        proxy_pass http://backend;
    }
}
SettingValueMeaning
rate=10r/s10 req/secAverage rate
burst=20+20 queueAllow short spikes
nodelayImmediateNo queuing, just rate limit
limit_conn 1010 connectionsConcurrent connection cap

Timeouts

Aggressive timeouts protect against slow clients and hung backends:

server {
    # Client timeouts
    client_body_timeout 12s;
    client_header_timeout 12s;
    send_timeout 10s;

    # Proxy timeouts
    proxy_connect_timeout 5s;    # Fail fast if backend is down
    proxy_read_timeout 30s;      # Wait for backend response
    proxy_send_timeout 30s;      # Wait for client to accept response
}

Putting It All Together

Here's a production-ready Nginx reverse proxy config combining all optimizations:

user www-data;
worker_processes auto;
worker_rlimit_nofile 16384;
pid /run/nginx.pid;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # Basic
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 100;
    server_tokens off;

    # Buffers
    client_body_buffer_size 16k;
    client_max_body_size 50m;
    client_header_buffer_size 2k;
    large_client_header_buffers 4 16k;

    # Gzip
    gzip on;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types application/json text/plain text/css text/javascript;
    gzip_proxied any;

    # Cache
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:100m max_size=2g inactive=60m use_temp_path=off;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    # Upstream
    upstream app_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;
        keepalive 64;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            limit_req zone=api_limit burst=20 nodelay;
            proxy_pass http://app_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 buffers
            proxy_buffer_size 16k;
            proxy_buffers 8 32k;
            proxy_busy_buffers_size 64k;

            # Timeouts
            proxy_connect_timeout 5s;
            proxy_read_timeout 30s;

            # Caching
            proxy_cache api_cache;
            proxy_cache_valid 200 10m;
            proxy_cache_use_stale error timeout updating;
            add_header X-Cache-Status $upstream_cache_status;
        }
    }
}

Conclusion

Tuning Nginx isn't about memorizing every directive--it's about understanding the three bottlenecks: connections (workers and keepalive), memory (buffers and caching), and bandwidth (compression and rate limiting).

Start with worker tuning and keepalive connections--these give the biggest immediate wins. Then add caching for read-heavy endpoints and Gzip for text responses. Finally, layer in rate limiting once you understand your traffic patterns.

Test every change with nginx -t before reloading. And benchmark: ab -n 10000 -c 100 https://yoursite.com/ before and after. You'll see the difference in the numbers.

#nginx#reverse-proxy#performance#caching#linux
D
DevToCashAuthor

Senior DevOps/SRE Engineer · 10+ years · Professional Trader (IDX, Crypto, US Equities)

I write about real infrastructure patterns and trading strategies I use in production and in live markets. No courses, no affiliate hype — just documentation of what actually works.

More about me →