PERFORMANCE
August 5, 2026

The Biggest Performance Killers on a Web Server

7 min read
Author
CloudStick Team
WordPress Engineer
Share this article
The Biggest Performance Killers on a Web Server
CloudStick
Performance
Killers

No Caching Layer at Any Level

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.

php -i | grep -E "opcache.enable |opcache.memory_consumption"
redis-cli ping
redis-cli info stats | grep keyspace
# opcache.enable = Off, or no PING/PONG from redis-cli -> that layer is missing

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.

N+1 Queries and Missing Indexes

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.

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
EXPLAIN SELECT * FROM wp_postmeta WHERE meta_key = '_price';
# type: ALL and rows: 400000+ in the output means no usable index

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 Pool Misconfiguration

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.

; in the pool .conf: pm.status_path = /status
curl http://127.0.0.1/status?full
# "listen queue" > 0 repeatedly = pm.max_children is too low
; in the pool .conf: slowlog = /var/log/php81cs-fpm.slow.log
; request_slowlog_timeout = 5s
tail -f /var/log/php81cs-fpm.slow.log

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.

Unoptimized Images and Render-Blocking Assets

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.

jpegoptim --max=82 --strip-all hero.jpg
cwebp -q 80 hero.jpg -o hero.webp
# open DevTools -> Network tab -> reload with cache disabled
# sort by Time; anything blocking above "First Contentful Paint" in
# the waterfall is a render-blocking candidate for async/defer

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.

Too Many Active Plugins and Hooks

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.

curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s Total: %{time_total}s\n" https://example.com

Oversold Shared Hosting Resources

Sometimes none of the previous five killers apply and the server is still slow, because the underlying compute was never really yours in the first place. Classic cPanel-style shared and reseller hosting sells the same physical CPU cores and disk I/O to hundreds of accounts on one box, betting that most sites stay idle most of the time. When several noisy neighbors spike at once, your perfectly optimized site slows down for reasons that have nothing to do with your code.

This is the one killer you cannot fix with a slow query log or a plugin audit — the evidence is CPU steal time and I/O wait that spikes independent of your own traffic, visible in `vmstat 1` as a nonzero `st` column, or simply a support ticket that gets a vague answer about "server load." It is also the reason CloudStick is built on a per-server model rather than shared, per-site hosting: every CloudStick-managed server is a VPS or cloud instance you provision and control, so the CPU, RAM, and disk shown on the dashboard are entirely yours, not a shared pool sold to strangers, and there is no per-site fee model incentivizing anyone to overcrowd the box.

If a server slows down at the same time every day for no reason your own logs explain, stop optimizing your code and start suspecting your neighbors.

A Practical Performance Audit Checklist

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.

1. php -i | grep opcache.enable -> is bytecode caching on?
2. redis-cli ping -> is object caching reachable?
3. tail /var/log/mysql/mysql-slow.log -> any query over 1s repeating?
4. curl 127.0.0.1/status?full -> is the FPM listen queue > 0?
5. DevTools Network tab, sort by size -> any single image over 300KB?
6. vmstat 1 -> nonzero "st" (steal) column?

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.

Leave a comment
Full Name
Email Address
Message
Contents