Performance work on a dedicated server has an order, and following it saves most of the effort. Measure what is actually short, fix the cheap software causes, then tune the platform. Reversing that order means tuning a server around an inefficiency you could have removed.
Measure before changing anything
Three resources, three different failures, three different fixes.
CPU saturated means real work is happening. Find the process before assuming you need more.
Memory exhausted means processes are being killed and the machine is swapping. This causes outages rather than slowness.
Both idle and still slow means you are waiting on disk or network, and no amount of application tuning changes it.
That last case is common and frequently misdiagnosed, hours get spent profiling code on a machine that is waiting for I/O.
Caching first, and it is not close
A cached page served by the web server never starts the application and never touches the database. On a content site this is an order of magnitude, and everything below is a detail beside it.
Verify a cache hit in the response headers rather than assuming. A caching layer configured but not engaging is common and looks identical to one that is working.
Then the database
Two things account for most database-related slowness, and both are cheap.
A missing index. Enable slow query logging and read it after a day. What you nearly always find is one query, running constantly, scanning a whole table. Adding that index frequently beats doubling the machine's memory.
A buffer pool sized for nothing. Stock database configuration is deliberately conservative so it starts anywhere. On a server with real memory it uses a fraction of what is available: raising it is often the largest single change.
Leave room for everything else. A database configured to use most of the memory on a machine also running a web server produces the opposite failure.
Then process limits
PHP process pool settings decide how many requests can be handled simultaneously, and both directions fail.
Too few workers means requests queue while the machine looks idle. Too many means memory is exhausted under load and the kernel starts killing processes.
Size the pool from measured memory per worker, not from a number in a tutorial written for a different machine.
Storage
NVMe and SSD are dramatically faster than spinning disks for the random reads a database performs. On a database-backed workload the storage type matters more than most tuning.
The RAID level matters here too: mirrored arrangements handle random writes considerably better than parity ones. Setting up RAID and storage goes into the trade.
Serve static files properly
Images, stylesheets and scripts should be served directly by the web server with long cache headers and compression, never through the application.
And check what is actually being sent. An image uploaded at camera resolution and displayed at 400 pixels wide costs bandwidth and CPU on every request, usually more than any server tuning recovers.
Change one thing at a time
Measure, change one setting, measure again with the same method.
Applying six changes from six articles at once means you cannot tell which helped, which hurt, and which to revert when something breaks a week later. That is how servers end up with configurations nobody can explain.
Before and after any of this, measuring it honestly is its own skill. How to Benchmark a Server Meaningfully goes into what to test and what to ignore.
Find what the machine is waiting for
A server that is slow while not busy is waiting on something, and naming what it waits for is the whole diagnosis.
vmstat 5 5
ps -eo state,pid,comm | awk '$1 ~ /^D/' | head
iostat -x 5 3 2>/dev/null | awk '/^[a-z]/ {print $1, $NF}'
Processes in uninterruptible sleep are blocked on storage. A machine with idle processors and a list full of them has a storage constraint, and adding cores changes nothing at all.
The same reasoning applies to network waits. A page assembling data from an external service spends its time on somebody else's response, and every local optimisation leaves that time exactly where it was.
Confirm the change helped before making another
Most tuning is never measured afterwards, which is why nobody can say which of six changes was the one that mattered.
for i in $(seq 1 20); do curl -s -o /dev/null -w '%{time_starttransfer}\n' "https://example.com/?r=$i"; done \
| sort -n | awk '{a[NR]=$1} END {printf "medyan %.3f p90 %.3f\n", a[int(NR/2)], a[int(NR*0.9)]}'
Record that before and after each individual change, on the same page and at the same time of day. Discard the first run, which measures a cold cache rather than the server.
Watch the ninetieth percentile alongside the median. A change that improves the typical case and worsens the worst one has made the site feel less reliable, and visitors notice inconsistency more than they notice an average.
Serve the files that do not need the application
A request for an image that passes through the application costs the whole application to answer, and most sites do this for at least some files.
curl -sI https://example.com/image.jpg | grep -iE 'server|x-powered-by|cache-control'
awk '$7 ~ /\.(jpg|png|css|js|woff2)$/ {c++} END {print c, "statik istek"}' ~/logs/example.com
awk '{print $7}' ~/logs/example.com | wc -l
Compare the two counts. On a typical site most requests are for static files, and if those are being handled by the application rather than served directly, most of the machine's work is unnecessary.
A response carrying application headers on an image is the signature. Serving those directly is usually a configuration change rather than a code change, and it is the single largest reduction available on many sites.
Find the work that happens on every request
Some cost is paid by every visitor regardless of what they asked for, and that is where the highest return is.
tail -100 ~/logs/example.com.error.log | grep -ciE 'warning|notice|deprecated' ls -la ~/public_html/.htaccess 2>/dev/null; grep -c . ~/public_html/.htaccess 2>/dev/null
A warning repeating on every page load is both a fault and a cost, since writing to the log is work done every time.
The same applies to anything loaded unconditionally: a configuration read from the database, a remote check, a large rules file evaluated per request. Each is small and each is multiplied by every visitor, which is what makes them worth more than a change affecting one page.