Nobody sticks around for a slow website. If your pages take too long to load, visitors leave, and in many cases, they’re gone before the content even finishes rendering. We see this constantly in support, and the fix is rarely as simple as it looks from the outside.
A metric that comes up in almost every one of these investigations is Time to First Byte, or TTFB. It’s basically the gap between a browser sending a request and the server sending anything back. As a rough rule of thumb, under 200ms is great, 200–500ms is liveable, and anything pushing past 600ms means something upstream is struggling.
Initial Observation
A customer came to us with a website that felt sluggish across the board. Before diving into the server side, we checked the obvious front-end suspects first: image sizes, theme weight, caching setup. Nothing stood out. The front end was clean, indicating the delay occurred before any content even left the server. You could actually see it sitting there in the browser’s Network tab, that long bar before the first byte arrived.
Identifying the Bottleneck
To get a clearer picture, we ran a curl timing breakdown:
curl -o /dev/null -s -w “DNS: %{time_namelookup}\nConnect: %{time_connect}\nSSL: %{time_appconnect}\nTTFB: %{time_starttransfer}\nTotal: %{time_total}\n” https://xyz.com
Output came back like this:
DNS: 0.021
Connect: 0.034
SSL: 0.083
TTFB: 2.154
Total: 2.521
DNS, connection, and SSL are all fast. The 2.15-second TTFB was the problem, and it was clearly happening at the server or application layer, not anywhere in between.
Step 1: Server Resource Analysis
The first thing was to get eyes on the server itself. We pulled up top, htop, vmstat, iostat -x, and free -h to see what was going on. The numbers weren’t pretty. CPU was sitting at 95% more often than not, PHP-FPM had run out of workers to handle new requests, load average had climbed well past anything reasonable, and memory was almost gone. Basically, the server was already at its limit just handling day-to-day traffic; there was nothing left in reserve.
Step 2: Web Server Review
We pulled the service status for both Apache and Nginx, then sat watching the access and error logs scroll by in real time. Right away, we could see requests regularly taking 2+ seconds. So we started making adjustments: more PHP-FPM workers, Keep-Alive switched on, worker process tuning, Brotli/Gzip compression, HTTP/2 enabled. Some of that helped. But the CPU was still climbing above normal after normal traffic, which told us we hadn’t found the real problem yet.
Step 3: PHP Performance
The site was WordPress with quite a few plugins piled on top, so slow logging felt like the right next move:
request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/slow.log
That pointed us to something almost immediately: one plugin was constantly hitting the database on every request, regardless of which page was being loaded or whether it needed to. We also reviewed the PHP version, confirmed that OPcache was actually doing anything, and reviewed memory limits and the max execution time. Getting PHP upgraded and OPcache working properly took a decent chunk off script execution times.
Step 4: Database Analysis
Next, we turned our attention to the database. SHOW PROCESSLIST showed us what was running, then we flipped on slow query logging:
slow_query_log = ON
long_query_time = 1
Three things stood out: indexes were missing on tables that really needed them, the same SELECT queries were firing repeatedly when they could have been cached, and WordPress had accumulated a huge number of autoloaded options that were being pulled on every page load, whether needed or not. We added the missing indexes, optimised the heavy tables, cleared expired transients, switched off unused plugins, and reduced autoloaded data. Backend processing time dropped noticeably.
Step 5: Caching
Up to this point, every request was going through the full PHP and MySQL stack. To prevent requests from hammering the backend, we implemented several caching mechanisms: FastCGI cache, Redis for objects, PHP OPcache, and browser-level caching. Full-page caching for unauthenticated users had the biggest single impact; those requests stopped hitting the backend entirely.
Step 6: CDN Optimisation
Static assets were already served from a CDN, but HTML responses were still served from the origin server. Once we tightened up the edge caching rules, far more requests started being answered by CDN nodes rather than bouncing all the way back to the origin. That alone took a good amount of pressure off the server and made a visible difference for visitors around the world.
Step 7: DNS and SSL
DNS was quick to rule out. dig and nslookup both returned a lookup time of around 20ms. DNS wasn’t adding any meaningful delay here. SSL came back clean too via openssl s_client: TLS 1.3 active, session reuse working, OCSP stapling in place. Neither was the problem.
Step 8: The Root Cause
Once we delved deeper into profiling, the real culprit emerged. A third-party WordPress plugin was making external API calls on every single page load. When those external services were slow, which they sometimes were, the entire page sat waiting. We cached the API responses, tightened the timeouts, removed unnecessary calls, and deferred a few lower-priority features. That was the fix that made the biggest remaining difference.
Results
MetricBeforeAfterTTFB2.15s210msTotal Load Time2.52s790msCPU Usage95%42%DB Queries per Page340112
Beyond the numbers, the site felt genuinely different, stable under traffic peaks and responsive in a way it hadn’t been before.
Preventive Measures
A few things worth keeping in place to avoid ending up back here:
- Keep continuous monitoring across CPU, memory, disk I/O, and load average, not just when something breaks.
- Regularly update PHP, MySQL, and web server packages.
- OPcache and other caching layers tend to be silently disabled after updates, so it’s worth checking they’re still active after any upgrade.
- Dip into slow query logs every now and then, not just when something has already gone wrong.
- Unused plugins and themes are dead weight; remove them rather than leaving them deactivated.
- Static assets belong on a CDN, not the origin server.
- External API calls are sneaky performance killers. Cache responses where you can, and push anything that isn’t essential to the initial page load to run later rather than making the user wait.
- HTTP/2 and HTTP/3 can sit idle if nobody actually enables them. Double-check they’re running, not just present on the server
- Run a performance test after any major update before going fully live.
Conclusion
High TTFB almost never comes down to a single factor. In our experience, it’s usually a combination of smaller problems stacking up across the server, application, and database layers. Working through each layer methodically and actually measuring the impact of each change, rather than assuming, is what got this site from 2.15 seconds down to 210ms. The numbers matter, but so does the stability that comes with them.

