Nginx tuning for high traffic worker gzip buffer cache

You've got Nginx running on your VPS, but under heavy load it starts serving 502s, latency spikes, or memory gets eaten alive. That's not the server's fault, it's the config. Out-of-the-box Nginx is conservative. To handle high traffic you need to tune worker processes, enable gzip, tighten buffer sizes, and set up caching. This guide walks through each parameter with real commands for Ubuntu 24.04 and Debian 12.
Prerequisites
- A Linux VPS running Ubuntu 24.04 LTS or Debian 12 with root or sudo access
- Nginx already installed (
nginx -vto verify version 1.24 or newer) - Your site's config lives in
/etc/nginx/sites-available/ - You understand what a restart does to active connections (plan a maintenance window)
Why tuning Nginx matters for high traffic
A default Nginx config is written for compatibility, not throughput. It uses one worker process, compresses nothing, and keeps small buffers. That works fine at 50 concurrent connections. At 500 or 5,000, the single worker becomes a bottleneck, gzip overhead can kill CPU, small buffers cause request failures, and the lack of cache means the backend works twice as hard. Tuning each of these areas is not optional at scale, it's what separates a VPS that stays up from one that falls over on a traffic spike.
Step 1 - Set worker processes and connections
The worker_processes directive controls how many Nginx worker processes handle connections. A common rule: set it to the number of CPU cores. worker_connections is the maximum number of simultaneous connections per worker.
Open the main config:
sudo nano /etc/nginx/nginx.conf
Find or add these lines in the events block:
worker_processes auto;
events {
worker_connections 1024;
multi_accept on;
use epoll;
}
worker_processes auto;, auto-detect CPU cores. On a 4-core VPS this spawns 4 workers. If you serve static files or a reverse proxy,autoworks perfectly. For CPU-bound SSL termination or heavy proxying, test withworker_processes 2 * coresbut watch memory.worker_connections 1024, 1024 per worker. On a 4-core box that's 4,096 concurrent connections. Bump to 2048 if Nginx handles keepalives and proxying.multi_accept on, accept all new connections at once, not one by one. Reduces latency under burst load.use epoll, event-driven I/O on Linux. Efficient for large numbers of connections.
Verify: After restart, check that workers match core count.
sudo nginx -t && sudo systemctl restart nginx
grep "worker_processes" /var/log/nginx/error.log
Step 2 - Enable and tune gzip compression
Gzip shrinks HTML, CSS, JS, and JSON by 60-80% before sending it over the wire. The CPU cost is minimal on modern hardware; the bandwidth saving is huge. Without it, each client downloads unnecessary megabytes.
In the http block of /etc/nginx/nginx.conf, add or uncomment:
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 4;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
gzip_comp_level 4, Level 4 hits the sweet spot: good compression ratio (around 70%) without burning CPU. Level 5-6 yields only 2-3% more compression for 20% more CPU. Stay at 4 for high traffic.gzip_min_length 256, Don't compress tiny responses (headers are under 256 bytes). Gzip overhead on a small file can make it larger than the original.gzip_proxied any, Compress responses when Nginx is a reverse proxy. Essential for proxied PHP or Node apps.gzip_vary on, AddVary: Accept-Encodingheader so intermediate caches know to serve gzipped vs non-gzipped versions.
Verify: Reload and test a response:
sudo nginx -t && sudo systemctl reload nginx
curl -H "Accept-Encoding: gzip" -I https://your-site.com | grep "Content-Encoding"
Expected output: Content-Encoding: gzip.
Step 3 - Tune buffer sizes
Buffer misconfiguration is a common cause of random 502s or truncated responses under load. Nginx uses buffers to read headers and body from clients and backends. If they're too small, Nginx writes to disk (slow) or drops the connection.
These settings live in the http block or inside a server block if you want per-site tuning:
client_body_buffer_size 128k;
client_max_body_size 10m;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
proxy_buffer_size 8k;
proxy_buffers 8 8k;
proxy_busy_buffers_size 16k;
client_body_buffer_size 128k, Buffer for POST/PUT request bodies. Default is 8k or 16k. If your app receives JSON payloads, 128k avoids disk writes for most requests. A VPS with 2GB RAM can easily handle this.client_max_body_size 10m, Reject bodies larger than 10MB (adjust for file uploads). Keep it reasonable to block large DDoS payloads.client_header_buffer_size 1k, Holds the request line and most headers. Most requests fit in 1k. If you get 400 errors on large cookies or long URLs, bump to 2k.large_client_header_buffers 4 8k, If the request line or a header exceedsclient_header_buffer_size, Nginx allocates one of these. 4 buffers of 8k each handle long URLs (think query strings with hundreds of params) and large cookies. If you see "414 Request-URI Too Large" or "400 Bad Request" in logs, increase this.proxy_buffer_size 8k, Size of the buffer used to read the first part of the response from the proxied server. Must match the upstream header size. 8k works for most PHP-FPM and Node backends.proxy_buffers 8 8k, Number and size of buffers for proxied response bodies. 8 buffers of 8k each (64k total). If your backend sends large responses, increase to8 16k.proxy_busy_buffers_size 16k, Buffer for sending to the client while data is still being read from the backend. 2x the per-buffer size is a safe formula.
Verify: After reloading, hit your server with a large request and check error logs:
sudo nginx -t && sudo systemctl reload nginx
curl -X POST -d "$(python3 -c 'print("a"*100000)')" -I https://your-site.com
sudo tail -f /var/log/nginx/error.log
If no "upstream sent too big header" or "client body too large" errors appear, your buffers are adequate.
Step 4 - Set up caching for static and proxied content
Caching offloads repeated requests from your backend. For static files (images, CSS, JS) you use expires headers. For proxied content (Django, Rails, PHP applications) you use proxy_cache. Both dramatically reduce load.
Static file caching
Inside your server block, add:
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
This tells browsers and CDNs to cache these files for 30 days. immutable means the file will never change, perfect for versioned assets (e.g., style.v2.css).
Proxy cache for dynamic content
Define a cache zone in the http block:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m max_size=1g inactive=60m use_temp_path=off;
levels=1:2, Two-level directory structure to avoid one big directory. First level: 1 hex char, second: 2 hex chars.keys_zone=mycache:10m, Store cache keys in memory. 10MB holds about 80,000 keys.max_size=1g, Hard limit on cache size. On a VPS with Linux VPS 2GB RAM, 1GB is reasonable. Adjust based on your traffic and disk space.inactive=60m, Remove files not accessed for 60 minutes. Prevents cache bloat.use_temp_path=off, Write cached files directly to the cache directory, not a temp location. Saves one I/O operation.
Then use the cache in a location block (e.g., for a PHP app behind PHP-FPM):
location ~ \.php$ {
proxy_cache mycache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 10m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_pass http://unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
}
proxy_cache_key, Cache key includes scheme, method, host, and URI. Avoids serving cached POST responses to GET requests.proxy_cache_valid 200 10m, Cache 200 responses for 10 minutes. Adjust based on content freshness requirements.proxy_cache_use_stale, Serve stale content if the backend is down or updating. This prevents a backend crash from taking down the site.
Verify: Hit the server twice and check the X-Cache header:
curl -I https://your-site.com/some-page 2>&1 | grep -i "X-Cache"
curl -I https://your-site.com/some-page 2>&1 | grep -i "X-Cache"
First request should show MISS, second HIT.
Step 5 - Finalize and reload
After all edits, test and reload.
sudo nginx -t
sudo systemctl reload nginx
Run a quick load test (install wrk or use ab):
wrk -t4 -c100 -d30s https://your-site.com
Compare requests per second and latency against the default config. You should see a 2x-3x improvement in throughput on the tuned config.
Common tuning mistakes
- Setting
worker_processestoo high, More workers than CPU cores causes context switching overhead. Stick toautoor test withnprocoutput. - Over-tuning buffer sizes upward, Each connection gets its own buffer set. A 1GB buffer per client would OOM a cheap VPS plan quickly. The values above are safe for any VPS with 1GB RAM or more.
- Enabling
sendfilewith proxied content,sendfile onworks great for static files (zero-copy I/O). But if you're proxying to an upstream, Nginx must read data into userspace anyway, sosendfilegains nothing and can cause issues. Keep itofffor proxy-heavy configs. - Not testing cache invalidation, If you enable proxy cache but your app doesn't send
Cache-Controlheaders, stale content gets served. Purge the cache after deployments withrm -rf /var/cache/nginx/*and a reload.
FAQ
How do I find the optimal worker_processes value?
Run nproc to see CPU core count. Set worker_processes auto; and Nginx matches it. For CPU-bound workloads (SSL, gzip) test with 2 * cores but monitor memory: each worker consumes your tuned buffer pool.
Should I enable sendfile for a reverse proxy?
No. sendfile bypasses userspace for disk I/O, but a reverse proxy reads data from network sockets, not disk. Enabling it does nothing useful and can cause partial transfers with some upstreams. Keep it off.
What gzip level should I use for APIs?
Level 4. API responses are often JSON, compressible by 70% at level 4 with negligible CPU. Above level 5 the compression ratio gains plateau while CPU usage jumps significantly.
How do I clear Nginx's proxy cache after a deployment?
Stop Nginx or take it out of the load balancer, then delete the cache: rm -rf /var/cache/nginx/*. Reload Nginx with systemctl reload nginx to rebuild the directory tree. Alternatively, configure proxy_cache_bypass to skip cache for specific cookies or IPs during testing.
I get "414 Request-URI Too Large" after tuning. What do I change?
Increase large_client_header_buffers. Start with 4 16k and retest. This is common when you have long query strings or large cookies from analytics scripts.
Related articles
- Enable HTTP/3 and Brotli compression on Nginx
- Reverse proxy with automatic HTTPS using Caddy
- Zero-downtime deploy with Nginx and systemd on Ubuntu 24.04


