Nginx is fast out of the box, but a few settings make a real difference on a busy VPS: compression, browser caching and, above all, a page cache so PHP does not rebuild the same page for every visitor. This guide covers practical Nginx performance tuning without risky tweaks.
Before you start
- A working Nginx + PHP-FPM site on a Linux VPS.
- A way to measure: browser developer tools,
curl -w, or a load-testing tool. Measure before and after each change. - Always run
nginx -tbefore reloading.
Worker processes and connections
In /etc/nginx/nginx.conf:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
}auto starts one worker per CPU thread. Total connections ≈ workers × worker_connections, far more than most VPSs need.
Keep-alive and timeouts
http {
sendfile on;
tcp_nopush on;
keepalive_timeout 30s;
keepalive_requests 1000;
client_body_timeout 15s;
send_timeout 15s;
server_tokens off;
}Shorter timeouts free connections held by slow or idle clients. server_tokens off hides the Nginx version.
Enable compression
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/javascript application/json image/svg+xml application/xml;Level 5 is a good balance between CPU and size. Brotli compresses text better but needs the separate ngx_brotli module. Images like JPEG, PNG and WebP are already compressed – do not gzip them.
Browser caching for static files
location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}If your build tool adds version hashes to file names, you can safely use much longer expiry times.
HTTP/2
Enable it on HTTPS listeners. On Nginx 1.25.1 and newer:
listen 443 ssl;
http2 on;On older versions use listen 443 ssl http2;.
Add a FastCGI page cache
- In the
httpblock:fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=PHPCACHE:100m max_size=1g inactive=60m use_temp_path=off; fastcgi_cache_key "$scheme$request_method$host$request_uri"; - In the server block, decide what must not be cached:
set $skip_cache 0; if ($request_method = POST) { set $skip_cache 1; } if ($query_string != "") { set $skip_cache 1; } if ($request_uri ~* "/wp-admin/|/wp-login.php|/cart|/checkout|/my-account") { set $skip_cache 1; } if ($http_cookie ~* "wordpress_logged_in|woocommerce_items_in_cart|comment_author") { set $skip_cache 1; } - In the PHP location:
fastcgi_cache PHPCACHE; fastcgi_cache_valid 200 301 10m; fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; add_header X-Cache $upstream_cache_status; - Create the folder, test and reload:
sudo mkdir -p /var/cache/nginx/fcgi && sudo nginx -t && sudo systemctl reload nginx.
Check with curl -I https://example.com: the second request should show X-Cache: HIT.
Warning: Never cache pages for logged-in users, carts or checkouts – visitors could see someone else's data. Test carefully on shops and membership sites.
Clearing the cache
Delete the cache folder contents (sudo rm -rf /var/cache/nginx/fcgi/*) after deployments, or use a WordPress plugin designed to purge Nginx FastCGI caches.
Troubleshooting
- X-Cache always MISS or BYPASS: a cookie or query string triggers the skip rules, or the app sends
Cache-Control: no-cache/Set-Cookieheaders. - Stale content after updates: lower
fastcgi_cache_validor purge after publishing. - "too many open files": raise
worker_rlimit_nofileand the systemdLimitNOFILEfor nginx.
FAQ: Nginx performance tuning
Is FastCGI cache the same as LSCache?
Both are server-level page caches. LSCache on LiteSpeed integrates with plugins that purge automatically; FastCGI cache needs your own purge strategy.
Should I raise worker_connections to the maximum?
No. Higher values only help if you actually have that many concurrent connections and enough file descriptors.
Does gzip slow down the server?
It uses some CPU, but at moderate levels the bandwidth and speed gains far outweigh it.
