
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
`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.
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.

