Nginx Performance Tuning on Ubuntu Linux for High-Traffic Websites

Nginx Performance Tuning on Ubuntu

If you run Nginx Performance Tuning on Ubuntu for a site that gets real traffic, the job is not “add a few directives and pray.” It is a layered system: Nginx, the kernel, PHP-FPM or your upstream app, caching, and the network stack all need to stop tripping over each other. I’ve seen sites look “fine” at 200 concurrent users and then fall apart at 800 because one tiny limit was still set like it was 2014.

Nginx is fast because it is event-driven, not because it is magical. Think of it like a very disciplined traffic cop: one cop can keep a lot of lanes moving if the road markings are clear and the junctions are not clogged. The catch is that the cop still needs enough file descriptors, enough worker capacity, and enough upstream headroom to keep moving traffic without stalling.

On Ubuntu, that means you usually need to tune both the web server and the operating system. Official Nginx documentation covers core directives like worker_processes, worker_connections, and worker_rlimit_nofile, while Ubuntu-side tuning usually touches sysctl, limits, and service defaults. If you only change one layer, the next layer becomes the bottleneck and steals all the blame.

Baseline first

Before touching config, measure what is happening. Use curl, ab, wrk, siege, htop, iostat, and ss to see whether the problem is CPU saturation, disk wait, upstream latency, or connection exhaustion. If you skip this, tuning turns into superstition.

When I first configured this on a production Nginx server behind WordPress, the site was not slow because of Nginx at all. The real issue was PHP-FPM queueing and an absurdly small file descriptor limit, so Nginx was basically waiting in line like everyone else. That kind of mistake is why I always start with baselines: request rate, TTFB, 95th percentile latency, open connections, and upstream response time.

A useful pattern is to test one change at a time and keep notes. Otherwise, you will never know whether the win came from cache, kernel tuning, or just the restart that cleared a mess.

Core worker tuning

Start with the worker model. worker_processes auto; is the safe default on modern Ubuntu because it matches the number of available CPU cores. worker_connections sets how many concurrent connections each worker can handle, but the real ceiling is also affected by open file limits and upstream connections.

A practical starting point for a busy site is:

user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;

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

epoll matters on Linux because it is the scalable event mechanism Nginx uses to juggle many sockets efficiently. multi_accept on; can help in some high-connection bursts, though it is not a silver bullet. I treat it like a sharper knife: useful in the right hands, annoying if you do not know what it is cutting.

The file descriptor limit is the quiet killer. If your Nginx worker can only open a few thousand files and sockets, it does not matter how heroic your worker_connections setting looks. That is why Ubuntu tuning often includes /etc/security/limits.conf, systemd defaults, and Nginx’s own worker_rlimit_nofile.

HTTP tuning

Once workers are sane, tune the HTTP layer. sendfile on; lets the kernel move static files efficiently, tcp_nopush on; helps packet coalescing for large file transfers, and tcp_nodelay on; keeps interactive responses snappy. These three are not decorative; they are part of the plumbing.

For keep-alive, the trick is balance. Too short and you waste handshakes; too long and you hold sockets open for clients that already left the building. A common starting point is:

keepalive_timeout 30;
keepalive_requests 1000;

Compression still matters, but it should be selective. Gzip is easy and widely supported, while Brotli often gives better compression on text-heavy assets, especially for WordPress front ends and theme CSS bundles. I usually enable gzip first, then test Brotli if the module is available and the CPU headroom is reasonable.

A quick example:

gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;

Do not compress already-compressed assets like JPEG, PNG, or MP4. That is like putting a suitcase inside another suitcase and calling it optimization.

Caching strategy

Caching is where a lot of high-traffic sites win or lose the day. A reverse proxy cache or FastCGI cache can flatten traffic spikes and reduce pressure on PHP or application servers. For WordPress, this is often the difference between “survives a mention on social media” and “falls over at the worst possible time.”

Browser caching is the first layer. Set sensible cache headers for static assets so repeat visitors do not re-download the same logo, CSS, and JS every time. Then move to server-side caching for pages that can be safely cached.

A simple mental model: browser cache is the pantry, server cache is the freezer, and your origin app is the cook. If everything comes from the cook, the kitchen catches fire. If the pantry and freezer are stocked, the cook only works when needed.

For PHP sites, fastcgi_cache is often the biggest win:

fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=5g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

location ~ \.php$ {
    fastcgi_cache WORDPRESS;
    fastcgi_cache_valid 200 301 302 10m;
    fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
}

Use cache bypass rules for logged-in users, carts, checkout, and admin paths. Caching dynamic pages blindly is one of the fastest ways to create a performance bug that looks like a content bug.

PHP-FPM and upstreams

If the site is WordPress, PHP-FPM is usually where the pain lives. Nginx can accept the traffic, but PHP-FPM has to generate the response. If the pool is too small, requests queue; if it is too large, memory pressure destroys the box.

For tuning, inspect your average process memory first, then size pools from there. A server with 8 GB RAM does not magically support 200 PHP workers just because a blog somewhere said “increase children.” A safer starting point looks like this:

pm = dynamic
pm.max_children = 24
pm.start_servers = 6
pm.min_spare_servers = 6
pm.max_spare_servers = 12
pm.max_requests = 500

This is where production reality bites. On one high-traffic WordPress stack I tuned, the site had plenty of CPU but still timed out during traffic bursts because the PHP pool was configured like a tiny café during lunch rush. Once the pool was right-sized and cache hits increased, the Nginx logs calmed down almost immediately.

Also pay attention to timeouts. Long timeouts can hide problems; short timeouts can break legitimate slow requests. The right answer depends on whether your app is mostly static, editorial, or ecommerce. WooCommerce is not a brochure site, and treating it like one is asking for pain.

Ubuntu kernel tuning

Ubuntu defaults are stable, not necessarily ideal for very high concurrency. For heavy traffic, you often want to raise connection backlogs and file descriptor ceilings, then verify those changes persist after reboot. That means thinking about sysctl and systemd, not just Nginx config.

Useful kernel-side starting points:

net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_fin_timeout = 15

somaxconn and tcp_max_syn_backlog are the bouncers at the door. If the queue is tiny, burst traffic gets rejected before it even reaches your workers. Raising them helps absorb spikes, but it should still be paired with application capacity and caching.

I also pay attention to conntrack on systems doing lots of proxying or firewall inspection. When the table fills, packets get dropped in a way that feels random until you inspect the counters. That is the sort of failure that makes people blame Nginx unfairly.

TLS and HTTP/2

TLS can be expensive, but modern Nginx on modern Ubuntu handles it well if you keep the config sane. Use TLS 1.2 and 1.3, prefer modern ciphers, enable session reuse, and turn on OCSP stapling where appropriate. HTTP/2 is still useful for multiplexing many assets over one connection, especially on content-heavy sites.

A simple HTTPS server block can look like this:

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;
}

My contrarian take: don’t obsess over TLS micro-optimizations before you fix caching and PHP-FPM. I have seen people spend half a day arguing about cipher order while the real bottleneck was a database query that took 900 ms. Pretty SSL looks nice; fast application design pays the bills.

HTTP/3 is worth testing if your audience is mobile-heavy and you have the operational maturity to support it, but it is not the first lever I pull on Ubuntu production boxes. Start with the boring stuff. The boring stuff usually wins.

Security without drag

Security and speed do not have to fight, but they often do when misconfigured. Rate limiting abusive IPs, blocking noisy endpoints, and trimming giant uploads can protect throughput without hurting real users. For WordPress, endpoints like /wp-login.php and /xmlrpc.php deserve special attention.

Example:

limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;

server {
    location /wp-login.php {
        limit_req zone=one burst=20 nodelay;
    }
}

Do not turn your web server into a fortress with no doors. A tight security stance should remove waste, not create a support ticket every time a human clicks a button.

Logging also matters. Extremely verbose logs can create unnecessary disk I/O on a busy server. Use a sensible log format, rotate aggressively, and archive what you actually need for incident response and SEO debugging.

Tuning priorities by site type

Site type Best first move Why it matters
Small blog gzip, keepalive, browser caching Low risk, quick gain
Busy WordPress FastCGI cache, PHP-FPM sizing, file limits Removes origin pressure
WooCommerce selective cache, PHP tuning, timeouts Avoids breaking carts and checkout
Reverse proxy worker limits, upstream keepalive, conntrack Connection handling becomes the main job

This table is the practical version of the “what should I tune first?” question. If your site is mainly editorial, caching and compression usually beat deep kernel work. If your site is a reverse proxy for many backends, connection tuning and descriptor limits move to the front of the line.

Checklist

Use this rollout order:

  1. Measure current performance and capture a baseline.
  2. Enable or verify worker_processes auto;.
  3. Increase file descriptor limits at the OS and Nginx level.
  4. Tune keep-alive, sendfile, and TCP socket directives.
  5. Add or refine caching.
  6. Right-size PHP-FPM or upstream concurrency.
  7. Apply kernel backlog and sysctl adjustments.
  8. Re-test with the same workload.
  9. Keep a rollback copy of every file you edit.

This is not glamorous, but it prevents “fixed one thing, broke three others” syndrome. That syndrome is common in production, especially when someone edits Nginx at 4:45 p.m. on a Friday.

Mistakes to avoid

The biggest mistake is tuning blindly. The second biggest is thinking Nginx can fix an overloaded database or a slow PHP app by itself. Nginx is a very good traffic manager, not a replacement for application design.

Another trap is overcaching dynamic pages. A cached homepage is great; a cached cart page is a disaster. A wrong cache rule can make a fast site feel broken faster than a slow site ever could.

The last mistake is using someone else’s config as gospel. Hardware differs. Traffic patterns differ. WordPress plugins differ. A copied tuning set is a starting point, not a guarantee.

Expert tips

Treat every change like an experiment. Change one thing, document the timestamp, then test again with the same load profile. I like to keep nginx -t, systemctl status nginx, ss -s, and application logs open while testing because the failure signal often shows up in a different place than the cause.

Watch these metrics:

  • worker utilization.
  • open file descriptors.
  • upstream response time.
  • 5xx and 499 rates.
  • cache hit ratio.
  • load average and iowait.

If you want one mental image, use this: performance tuning is not a race car upgrade. It is more like aligning the wheels, adjusting tire pressure, and making sure the engine actually has fuel. A faster engine helps, but only if the rest of the car is not dragging sideways.

FAQ

How many worker processes should I use?

Use worker_processes auto; on Ubuntu unless you have a very specific workload and have benchmarked a manual value. That setting usually maps well to available CPU cores and avoids underusing the machine. If you serve lots of static content or do heavy TLS work, test carefully before changing it.

Is gzip enough, or do I need Brotli?

Gzip is enough to start, and it is easy to maintain. Brotli can give better compression for text assets like CSS and JS, but it adds operational complexity and sometimes more CPU cost. For most high-traffic WordPress sites, gzip first and Brotli second is the sane order.

Should I tune Nginx before PHP-FPM?

Tune both, but if the site is dynamic, PHP-FPM often matters more after the basics. Nginx can accept connections quickly, yet your app can still bottleneck on PHP workers or database latency. Fix the actual queue first, not just the front door.

Does caching always improve performance?

No. Caching improves performance only when the cache policy matches the content. Cached editorial pages are usually great, while cached personalized content, checkout flows, and login pages can create broken behavior. Good caching is selective, not universal.

What is the safest first change?

worker_processes auto; is usually the safest first change, followed by sensible file descriptor limits and basic keep-alive tuning. Those changes are low risk and often deliver immediate gains. After that, move into caching and upstream sizing.

Marshall Anthony is a professional Linux DevOps writer with a passion for technology and innovation. With over 8 years of experience in the industry, he has become a go-to expert for anyone looking to learn more about Linux.

Related Posts