SCALING
August 5, 2026

How to Handle Traffic Spikes Without Crashing

6 min read
Author
CloudStick Team
WordPress Engineer
Share this article
How to Handle Traffic Spikes Without Crashing
CloudStick
Survive The
Traffic Spike

What Breaks First When Traffic Spikes

A traffic spike does not take a server down all at once — it fails one layer at a time, in a fairly predictable order, and knowing that order tells you exactly where to look and what to fix first instead of guessing under pressure.

1. PHP-FPM pool exhausts pm.max_children
-> new requests queue, then time out as 502/504
2. MySQL hits max_connections
-> "Too many connections" errors, PHP workers stall waiting on DB
3. Disk I/O saturates from cache misses hitting the DB at once
-> queries that were fast under low load suddenly crawl
4. RAM runs out, OOM killer starts terminating processes
-> PHP-FPM or MySQL itself gets killed, site goes fully down

The first failure is almost always PHP-FPM: `pm.max_children` is a hard concurrency ceiling, and once every worker is busy, new requests do not get dropped politely — they queue in the socket backlog and then time out, which is what shows up to visitors as a 502 or 504. If the site somehow survives that, the next thing to give is MySQL, because more PHP workers all trying to run queries at once means more simultaneous connections, and `max_connections` is a hard limit too. If both of those hold, the spike often pushes past your cache hit ratio, so a flood of uncached requests all land on the database at the same moment and disk I/O saturates. RAM exhaustion and the OOM killer are the last and worst stage — by the time you are there, the fix is no longer tuning, it is triage. Everything below is aimed at pushing that failure point further out, or avoiding it entirely.

Pre-Provision PHP-FPM and MySQL for Peak, Not Average

Most `pm.max_children` values are set once, based on whatever the default was or what average daily traffic needed, and never revisited. That number needs to reflect your expected peak concurrency, not your average — average traffic is exactly the load a spike is not.

# /etc/php83cs/fpm/pool.d/<sitename>.conf
pm = dynamic
pm.max_children = 80
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
# /etc/mysql/mariadb.conf.d/50-server.cnf
max_connections = 300

A reasonable starting point for `pm.max_children` is available RAM for PHP divided by the actual average memory size of one PHP-FPM worker process, which you can check with `ps --sort=-rss -eo rss,comm | grep php` under normal load. Size `max_connections` in MariaDB to comfortably exceed the peak number of PHP workers that could be running queries simultaneously, and confirm current usage with `SHOW STATUS LIKE 'Threads_connected';` during a busy period rather than guessing.

WARNING

Raising `pm.max_children` without checking RAM first does not fix a spike — it just changes which resource runs out. Each additional worker holds its own chunk of memory, so pushing the ceiling up during an incident can exhaust RAM and trigger the OOM killer faster than the original 502s would have happened, taking down PHP-FPM or MySQL entirely instead of just queuing requests.

Cache So Most Requests Never Reach PHP or MySQL

The cheapest way to survive a spike is to never let most of the traffic reach PHP-FPM or MySQL at all. A page cache serves a static HTML copy of a rendered page directly from Nginx, skipping the PHP process pool and every database query behind it entirely.

# nginx-cs page cache snippet
fastcgi_cache_path /var/cache/nginx-cs levels=1:2 keys_zone=WPCACHE:100m inactive=60m;
location ~ \.php$ {
fastcgi_cache WPCACHE;
fastcgi_cache_valid 200 60m;
fastcgi_cache_use_stale error timeout updating;
}

Pair page caching with Redis as the object cache layer for anything that still has to hit PHP — logged-in sessions, cart pages, search results — so repeated database lookups are served from memory instead of triggering a fresh MariaDB query every time. The `fastcgi_cache_use_stale` directive is worth calling out specifically for spike scenarios: if the origin is slow or briefly erroring while the cache is being regenerated, Nginx keeps serving the last good cached copy instead of piling more requests onto an already-struggling PHP-FPM pool.

Put Cloudflare or a CDN in Front of the Origin

Server-side caching still means every single request lands on your origin. A CDN in front absorbs static assets — images, CSS, JS — at the edge so those never touch your server at all, and it can cache a fair amount of your page cache at the edge too, so a spike is partly absorbed before it ever reaches Nginx.

CloudStick's built-in Cloudflare integration handles this without a separate account or a manual DNS migration — enabling it in the dashboard puts edge caching and DDoS protection in front of the origin, which matters most during exactly the kind of sudden, high-concurrency burst that would otherwise exhaust `pm.max_children` in minutes. A layer that absorbs the spike before it reaches PHP-FPM is worth more during an incident than any amount of tuning done after the fact.

TIP

While a spike is unfolding, open the CloudStick dashboard's real-time CPU, RAM, and disk graphs, powered by Zabbix Agent 2, to watch resource usage climb live. Seeing RAM trend toward the ceiling before the OOM killer acts gives you a window to intervene rather than finding out after processes have already been killed.

Rate Limit Abusive Traffic in Nginx

Not every spike is legitimate. Scrapers, bad bots, and credential-stuffing attempts against wp-login.php all count against the same finite pool of PHP-FPM workers as real visitors, and they are usually the easiest concurrency to shed before it matters.

# in the nginx-cs http block
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
# in the server or location block
location / {
limit_req zone=perip burst=20 nodelay;
}
location = /wp-login.php {
limit_req zone=perip burst=3 nodelay;
}

`limit_req_zone` tracks requests per client IP and `limit_req` enforces the rate at the location level, rejecting excess requests with a 503 before Nginx ever hands them to PHP-FPM. A tighter zone on `wp-login.php` specifically is worth the extra block since brute-force login attempts are one of the most common sources of sustained, low-value concurrency eating into your real capacity during a legitimate spike.

Build a Spike Playbook Before You Need It

Everything above reduces how likely a spike is to break anything. The playbook is what you do when it happens anyway, and writing it down ahead of time turns an incident into a checklist instead of an improvisation exercise under pressure.

A working playbook covers: a pre-calculated, RAM-safe number to temporarily raise `pm.max_children` to, rather than guessing a number live; a maintenance-mode or "high traffic, please retry" static fallback page ready to enable for non-critical pages so PHP-FPM capacity is reserved for checkout or signup flows; the exact dashboard panel to watch, CloudStick's live CPU, RAM, and disk graphs, so you know within seconds whether a change helped or made things worse; and a rollback step for every change, so a temporary tuning tweak does not become a permanent, unreviewed config drift.

In order of what to check when things start slowing down: confirm whether Nginx is returning 502/504 (PHP-FPM exhausted) or the PHP-FPM log shows MySQL connection errors (database exhausted); check disk I/O and cache hit ratio next; and only then look at RAM. Page caching, a CDN or Cloudflare in front, and Nginx rate limiting are what keep you from reaching this checklist in the first place — the playbook is only for the traffic that gets through anyway.

Leave a comment
Full Name
Email Address
Message
Contents