How to Diagnose Why Your Site Slows Down Under Load
Your site slows down under load because something in the request path stops scaling linearly once concurrency rises, and the fix depends on which layer that is. There are only three realistic culprits: a host limit you are hitting (CPU, memory, worker count, connection count), a database that serialises work or runs out of connections, or a dynamic page that is being rebuilt from scratch for every visitor because nothing caches it. This article gives you a repeatable method for telling those three apart, so you stop guessing and start fixing the right layer when you ask why does my site slow down under load.
Start by measuring latency, not CPU
Before you change anything, get a baseline that separates "how long a request takes" from "how many requests finish". Those two numbers diverge in informative ways. A host CPU limit makes every request slower in proportion to load. A database bottleneck makes a small subset of requests very slow while the rest stay fast. A missing cache makes all dynamic requests slow but leaves static assets untouched.
Hit the site with a fixed concurrency and watch the latency distribution, not just the average. curl in a loop is enough to start:
for i in $(seq 1 20); do
curl -s -o /dev/null -w '%{time_total}\n' https://example.com/dynamic-page
done | sort -n
The output is a sorted list of total times in seconds. If the fast end is tight and the slow end trails off, you have a queue somewhere. If everything shifts up together, you are probably saturating a shared resource like CPU or a worker pool. Run the same loop against a static file and against your dynamic page and compare the two distributions. A gap between them points at your application or database, not the network or the web server itself.
Check whether you are hitting a host limit
The fastest way to rule out the host is to look at what the machine is doing while the load runs. On the server, watch CPU, memory, and the number of processes or threads serving requests. top or htop gives you a live view; vmstat 1 shows whether you are waiting on CPU or on I/O. If the CPU is pinned and the run queue is long, the host is the limit and no amount of caching will save you.
Pay attention to the web server's own concurrency ceiling. With a process-based server, a small worker count means requests queue behind each other even when the CPU looks idle. Check the config directive that sets it. For a common setup it looks like this:
# number of worker processes or threads
worker_processes auto;
worker_connections 1024;
keepalive_timeout 65;
If worker_connections times the worker count is smaller than your peak concurrent connections, you will see requests wait for a free slot. That shows up as rising latency with flat CPU, which is easy to misread as a slow application. Also check memory: if the box starts swapping under load, everything slows at once and the pattern looks like a host limit even when the real problem is a memory-hungry process.
Separate database time from application time
If the host has headroom, the next question is whether requests are waiting on the database. The clean way to find out is to time the query, not the page. Turn on slow query logging and set a threshold low enough to catch the requests you care about, then reproduce the load and read the log.
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;
SHOW VARIABLES LIKE 'slow_query_log_file';
Every query slower than the threshold lands in that file with its duration and rows examined. If the slow log fills up during your load test, the database is your bottleneck. Two things commonly cause it: a missing index that turns a lookup into a full scan, and connection exhaustion, where the application opens more connections than the database allows and requests wait for a free one. Check the connection count against the configured maximum while the load runs. If connections are maxed out, the fix is pooling or reducing concurrency, not a faster query.
You can also confirm from the application side by logging the time spent in the database per request. If page time minus database time is small, the database owns the problem. If page time is large but database time is small, the time is going somewhere else, usually rendering or waiting on an external call.
Find out whether the dynamic page is cached at all
The third culprit is the quiet one: a page that is dynamic for every visitor and cached for none. Under light traffic it looks fine because the work is cheap enough. Under load the same work repeats for every request and the cost multiplies. Check the response headers on the page you are testing:
curl -sI https://example.com/dynamic-page | grep -i -E 'cache-control|age|x-cache|expires'
If you see Cache-Control: no-store or private, or no caching header at all, every request is going to your application. An Age header greater than zero means a shared cache served it; its absence on a page you expect to be cached is a red flag. Compare the headers on a static asset with the headers on your dynamic page. If the static file is cached and the dynamic page is not, you have found a layer that will not scale, and the fix is a cache in front of the application or a cache key inside it.
Watch out for pages that are technically cacheable but vary on something that defeats the cache, such as a session cookie or a query string that changes per visitor. Those pages will miss the cache every time and behave exactly like an uncached page. Check what the cache key includes before you assume caching is working.
Put the three tests in order
Run them in this sequence and you will usually stop at the first real answer. First, measure latency distributions under fixed concurrency to see whether the slowdown is uniform or a long tail. Second, watch CPU, memory, and worker or connection counts on the host to rule out a hard limit. Third, read the slow query log and connection count to rule out the database. Fourth, inspect cache headers on the page that is slow to rule out an uncached dynamic path. Each test is cheap and each one eliminates a layer, so you converge on the cause instead of changing several things at once and learning nothing.
What to do next: pick one page that gets slow, run the four checks against it while you apply load, and write down the numbers before you touch any config. Keep that baseline so you can prove a change helped. Fix the layer the evidence points at, one change at a time, and re-run the same test after each change. If you cannot reproduce the slowdown on demand, that is itself the first problem to solve, because you cannot fix what you cannot measure.
