Advanced Nginx Performance Tuning on an AlmaLinux VPS

Your Nginx on an AlmaLinux 9 VPS starts to sweat around a few hundred concurrent connections. Requests queue, latency climbs, and the CPU idles because the worker is blocked on disk or on a slow upstream. This is exactly the situation where you tune Nginx instead of buying a bigger Linux VPS. In this guide I walk through the settings that matter under real load for traffic inside Vietnam, with commands that run as-is on AlmaLinux 9.
- Set
worker_processesto the vCPU count, not "auto" if you can avoid the overhead. - Raise
keepaliveon the upstream block to reuse connections to PHP-FPM or a backend. - Enable gzip with sensible minimum lengths, and brotli if your Nginx build supports it.
- Offload static assets with
open_file_cacheand a fastcgi microcache.
Prerequisites
- An AlmaLinux 9 VPS with root or a sudo user. AlmaLinux 10 works too, the paths are identical.
- Nginx installed, either from the AppStream repo or nginx.org. I assume the distro package for this guide.
- A domain pointing to your VPS with a valid TLS certificate. Certbot works fine on AlmaLinux.
- Traffic that actually justifies tuning. If you serve 50 requests a day, skip this and read the basics.
Why Nginx Defaults Fall Short on High Traffic
The stock nginx.conf that ships with AlmaLinux is written for a small server. It sets one worker, a conservative worker_connections value, and no caching. On a server with 4 vCPUs and 8 GB of RAM, that means your hardware idles while a single process handles every connection. For traffic inside Vietnam, where round trips to the user add 20-40 ms over the domestic backbone, you want every request to finish in as few passes as possible.
The other gap is upstream handling. If you proxy to PHP-FPM or a Node backend, Nginx opens a new connection per request by default. That handshake cost adds up fast. The fix is a persistent upstream connection pool, which this guide enables. These are the settings I run on production boxes that push a few thousand requests per second without breaking a sweat.
Step 1 - Set Worker Processes and Connections
Open the main config and adjust the events block. The single most impactful value is matching worker_processes to your vCPU count. Check it first with nproc.
nproc
sudo nano /etc/nginx/nginx.conf
Set the values at the top of the file:
worker_processes 4;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
worker_rlimit_nofile raises the file descriptor limit so each worker can hold more open sockets. multi_accept tells a worker to grab all new connections in its queue at once, which reduces wakeups. epoll is the correct event model on Linux; Nginx picks it automatically, so setting it explicitly only documents intent.
After editing, test and reload:
sudo nginx -t
sudo systemctl reload nginx
Verify the worker count took effect:
ps -eo pid,ppid,comm | grep nginx
You should see one master process plus four workers for the 4 vCPU example. If you see only one worker, the config path is wrong or Nginx picked up a different file.
Step 2 - Tune Keepalive for Upstream Connections
With PHP-FPM or a reverse-proxied backend, Nginx opens a fresh TCP connection for every request unless you tell it otherwise. That adds a full round trip per request. Inside Vietnam, cross-provider routing between your VPS and the user already costs latency, so do not add more on the backend side.
In your server block or the included conf, the upstream section should look like this:
upstream php_backend {
server unix:/run/php-fpm/www.sock;
keepalive 32;
}
server {
location ~ \.php$ {
fastcgi_pass php_backend;
fastcgi_keep_conn on;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
keepalive 32 holds 32 idle connections per worker to the socket. fastcgi_keep_conn on tells Nginx to reuse them. Without both, the setting does nothing. For an HTTP proxy to a Node or Go service, the same idea applies with proxy_http_version 1.1; and proxy_set_header Connection "";.
Reload and check for errors in the log:
sudo nginx -t && sudo systemctl reload nginx
sudo tail -f /var/log/nginx/error.log
Step 3 - Enable Gzip and Brotli Compression
Text compresses well, and for users on mobile networks in Vietnam, smaller payloads mean faster loads. Enable gzip with a minimum length so you do not waste CPU on tiny responses.
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml;
gzip_vary on;
Level 5 is the sweet spot. Levels 6-9 crush CPU for a few percent more compression. For API responses that carry JSON, the gain is still worth it. gzip_vary on adds the Vary: Accept-Encoding header so caches store separate copies.
If your Nginx was built with brotli, prefer it over gzip for modern browsers. Check with:
nginx -V 2>&1 | grep -o brotli
If it prints brotli, add:
brotli on;
brotli_comp_level 5;
brotli_min_length 1024;
brotli_types text/plain text/css application/json application/javascript text/xml image/svg+xml;
Verify compression actually happens:
curl -H "Accept-Encoding: gzip" -I https://your-domain.com/ | grep -i content-encoding
You should see content-encoding: gzip (or br) in the response headers. If not, the asset is under the minimum length or the Content-Type is not in your list.
Step 4 - Cache Static Files and Microcache Dynamic Pages
Static assets like CSS, JS, and images should never hit PHP. Enable open_file_cache so Nginx keeps file metadata in memory, avoiding a stat() syscall per request, which matters on NVMe but still saves CPU.
open_file_cache max=4096 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
For logged-out users, a fastcgi microcache turns expensive PHP renders into static responses for a few seconds. This alone cut load on a Vietnam-facing WooCommerce box I run by more than half.
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=wpcache:32m inactive=5m max_size=512m;
server {
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-json/") { set $skip_cache 1; }
if ($http_cookie ~* "wordpress_logged_in") { set $skip_cache 1; }
location ~ \.php$ {
fastcgi_cache wpcache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout updating http_500;
fastcgi_cache_valid 200 60s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
}
}
The if rules skip caching for logged-in users, POST requests, and admin pages. fastcgi_cache_valid 200 60s serves fresh copies for 60 seconds. Tune the duration to how often your content changes.
Create the cache directory and reload:
sudo mkdir -p /var/cache/nginx
sudo chown nginx:nginx /var/cache/nginx
sudo nginx -t && sudo systemctl reload nginx
Verify the cache fills by hitting the site and checking the header:
curl -I https://your-domain.com/ | grep -i x-fastcgi-cache
Expect HIT on the second request, MISS on the first. If you see nothing, the cache key or bypass rules are off.
Step 5 - Enable HTTP/3 and Tune the SSL Session Cache
For users in Vietnam on mobile networks, HTTP/3 over UDP cuts connection setup time noticeably. AlmaLinux 9 ships Nginx 1.30.x, which supports HTTP/3. Enable it in the listen directive of your TLS server block.
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:32m;
ssl_session_timeout 1d;
add_header Alt-Svc 'h3=":443"; ma=86400';
reuseport lets multiple workers accept on the same UDP socket. Without it, HTTP/3 performs poorly. The Alt-Svc header tells browsers to try HTTP/3 next time. Validate the response:
curl -I --http3 https://your-domain.com/ 2>/dev/null | head -n 1
Older curl builds lack --http3; if the flag fails, check with a browser devtools panel under the protocol column. It should show h3.
| Parameter | Recommended value | Why it matters |
|---|---|---|
| worker_processes | vCPU count | Matches workers to hardware cores |
| worker_connections | 4096 | Handles concurrent keepalive sockets |
| keepalive (upstream) | 32 | Reuses backend connections |
| gzip_comp_level | 5 | Compression vs CPU trade-off |
| fastcgi_cache_valid | 60s | Microcache TTL for dynamic pages |
| ssl_session_cache | 32m | Reuses TLS handshakes, ~8k sessions |
Troubleshooting Common Tuning Failures
The most common failure after changing workers is a config test that passes but no visible effect. Check the running process list, not the config file. If Nginx keeps showing one worker, you edited the wrong config, usually a symlink or include path you missed. Run sudo nginx -T to dump the full effective config and confirm your values are present.
If the fastcgi cache never returns HIT, the $skip_cache rules are too broad. Test by commenting out the if blocks one at a time. Another culprit is the fastcgi_cache_key missing the host, which collides across domains on a multi-site box. The format I gave includes host and method, so copying it exactly avoids that trap.
For HTTP/3, the usual failure is curl reporting connection refused on UDP. Confirm the firewall allows it. On AlmaLinux, firewalld is the default, so open the UDP port explicitly:
sudo firewall-cmd --permanent --add-port=443/udp
sudo firewall-cmd --reload
sudo firewall-cmd --list-ports
If the port is open and browsers still use HTTP/2, clear the browser cache or use a private window. Browsers cache the Alt-Svc hint and stick to the older protocol until it expires.
FAQ
How do I check my vCPU count on an AlmaLinux VPS?
Run nproc to print the number of processing units available to the current process. On a virtual machine, this reflects the vCPU allocation from the hypervisor, which is the value you want for worker_processes.
Is brotli better than gzip for Nginx?
Brotli compresses text roughly 15-20% smaller than gzip at the same level, which matters on mobile networks in Vietnam. Use it when your Nginx build includes the module, but keep gzip enabled as a fallback for older clients that do not send Accept-Encoding: br.
Why does my fastcgi cache show MISS on every request?
Your bypass or skip rules are too broad, or the cache key changes per request due to a timestamp or cookie in the URI. Start with the exact key format in this guide and test with a single curl command before expanding to production traffic.
What is the safe maximum for worker_connections on a 4 GB VPS?
Start at 4096 and watch memory with free -h under load. Each connection holds buffers and request state; with 4 GB of RAM and PHP-FPM, 8192 is usually the practical ceiling before you swap. Raise worker_rlimit_nofile to match, or Nginx will hit the descriptor limit first.
Does HTTP/3 actually help for users inside Vietnam?
It helps most on high-loss mobile networks and when users are far from the server, because QUIC avoids head-of-line blocking. Inside Vietnam with domestic routing, the gain is modest but real, often shaving 30-80 ms on the first byte over cellular. On fiber with low loss, the difference is barely measurable.
Related articles
- Nginx tuning for high traffic
- Enable HTTP/3 and brotli compression on Nginx
- Set up a LEMP stack on Ubuntu
- Optimize PHP-FPM for WordPress
AlmaLinux 高流量 Nginx 调优要点
本文介绍了在 AlmaLinux 9 VPS 上针对越南高流量场景的 Nginx 调优方法。核心操作包括:将 worker 进程数设为 vCPU 数量、开启上游 keepalive 连接池、启用 gzip 压缩、为静态文件配置 open_file_cache、为动态页面设置 fastcgi 微缓存,以及启用 HTTP/3 协议。这些调整能显著降低延迟并减少 CPU 占用,适合面向越南用户的业务部署。


