
The single biggest recurring cause of a slow server is that it caches nothing, at any of the three layers that matter: no page cache, no object cache, and OPcache left disabled or at defaults. Every one of those layers exists to avoid redoing expensive work, and a server missing all three redoes the same work on every single request, all day, forever.
Without a page cache, every visitor to the same article or product page triggers a full PHP bootstrap, every plugin hook, and every database query that built that page for the last visitor. Without an object cache like Redis, results from the same expensive query get recomputed for every request that needs them instead of being read from memory in under a millisecond. Without OPcache, PHP recompiles the same script source into bytecode on every request, burning CPU that never shows up as an obvious error — it just quietly caps how many requests per second the server can serve.
A server with no caching layer isn't slow because it's underpowered — it's slow because it's solving the same math problem from scratch on every single request.
A single unindexed or N+1 query can dominate time-to-first-byte for every request that touches it, even on a server with idle CPU and free RAM to spare. This is the killer that most confuses people, because the server "looks fine" on a top or htop dashboard right up until you look at what a single request is actually waiting on.
N+1 happens when code loops over a result set and fires one query per row instead of one query for the whole set — a product listing that runs a separate query for each item's category, for example. A missing index turns what should be an indexed lookup into a full table scan, and that cost scales with table size, so a query that was fine at 10,000 rows can quietly become the slowest thing on the server at 500,000 rows. The fix for both starts with finding the actual slow query, not guessing at it.
Let the slow query log run under real traffic for a day, then sort it by duration rather than frequency — one query that runs twice a minute at eight seconds each is a bigger TTFB problem than a thousand queries that each take five milliseconds. Add a composite index on the columns actually used in the WHERE and JOIN clauses, and re-run EXPLAIN to confirm the type changes from ALL to ref or range before assuming the fix worked.
PHP-FPM fails in exactly two opposite directions, and both look like "the server is slow" from the outside. Too few workers (pm.max_children set too low) means requests queue up behind each other under load, adding seconds of wait time even though the box has free CPU. Too many workers means each PHP process claims its own chunk of RAM, and under real traffic the pool exhausts memory and the kernel starts killing processes, which shows up as intermittent 502s rather than a clean slowdown.
Diagnose queueing with the FPM status page, and diagnose which specific requests are slow with the FPM slow log — both need to be enabled explicitly, they are not on by default.
Sizing pm.max_children correctly means dividing the RAM you are willing to give PHP by the average memory footprint of one worker under real load, not by guessing a round number. This is exactly the setting CloudStick exposes per site — each site already runs its own isolated PHP-FPM pool (php81cs-fpm through php84cs-fpm) with its own open_basedir, and you tune pm.max_children, pm mode, and the slow log for that one site from the dashboard, without SSHing in to hand-edit a pool .conf file and risk a typo that takes down every other site on the box.
A perfectly tuned backend still feels slow if the browser has to download a 4MB hero image or wait on render-blocking CSS and JS sitting in the head before it can paint anything. These two killers live entirely on the front end, and no amount of server-side caching fixes them.
Uncompressed, uncropped images are the most common single offender on real-world sites — a photo straight out of a camera or stock library, dropped into a page at display size without resizing or compressing it first. Render-blocking resources are the second: any `<script>` or `<link rel="stylesheet">` in the head without `async`, `defer`, or a media query forces the browser to stop and fetch it before it can render a single pixel, even if that CSS or JS is only needed for something below the fold.
Every kilobyte you don't ship is a kilobyte the browser never has to wait on — front-end weight is a performance killer the server can't fix for you.
Every active WordPress plugin registers hooks on `init`, `wp_head`, or `template_redirect` that run on every single request, whether or not that specific page needs them — a checkout plugin's hooks still fire on your blog posts, and a form plugin's scripts still load on pages with no forms. Thirty active plugins do not mean thirty times the work of one plugin; it means thirty separate chances for a slow database call, an external API request, or a poorly written hook to add latency to every page on the site.
Diagnose this with Query Monitor rather than guessing: it shows the total query count and time per plugin for the current page load, right in the admin bar. Deactivate plugins in batches of five and re-measure TTFB with curl's timing output — one plugin doing a synchronous HTTP call to an external API on every page load is a common and easy-to-miss culprit, and it will jump out clearly once you isolate it.
Run through these six checks in order before touching anything — each one takes minutes and rules out (or confirms) one entire category of the killers above rather than making you guess.
Whichever check comes back positive is almost always the actual bottleneck, not the one you assumed going in. CloudStick's Web Application Logs and the per-server CPU, RAM, and disk dashboard exist specifically to make steps one, three, and four visible without SSH at all, so you can point the audit at the right killer in minutes instead of spending an afternoon reading config files that were never the problem.

