Why Is My WordPress Site Slow? How to Find the Cause
In this article10 sections
A slow WordPress site is almost always slow in one of four places: the network and web server before PHP runs, PHP itself (plugins, theme and workers), the database, or the size of the page the browser has to download. Measuring time to first byte (TTFB) at the origin tells you which half of that list to look at, and every step below uses a command you can run on the server without changing anything. The numbers come from the SiteHarbour blog itself, a WordPress site on a 1 GB Ubuntu 24.04 server running Nginx, PHP 8.4 and MySQL, so treat them as a reference for a lean site rather than a typical result.
The steps assume SSH access and WP-CLI. If you only have shared hosting without a shell, steps 1 and 5 work from any machine with curl, and the rest map to what your host’s panel shows. On cPanel hosting, five checks for a slow WordPress site on cPanel covers the same ground from the control panel. This guide builds on the full 1 GB WordPress optimization guide and the Nginx FastCGI cache guide.
Step 1: Measure time to first byte at the origin
TTFB is the time from sending a request to receiving the first byte of the answer. It includes the web server, PHP and the database, and none of the page’s images, scripts or rendering. That makes it the quickest way to split the problem: if TTFB is low and the page still feels slow, the cause is in the browser’s work, not the server. Run this on the server itself, which skips DNS, your network and any CDN in front:
curl -s --resolve example.com:443:127.0.0.1 -k -o /dev/null -w "ttfb=%{time_starttransfer}s total=%{time_total}s bytes=%{size_download}\n" https://example.com/
Replace example.com with your domain (both places) and run the command five times, because a single run can be a cold start. The same test on a blog post here gave:
| Request | Runs | Origin TTFB | X-Cache |
|---|---|---|---|
| Normal URL | 5 | 5.1 to 5.4 ms | HIT |
| Same URL with a random query string | 3 | 50 to 58 ms | BYPASS |
As a rough guide, Google’s performance guidance treats a TTFB of about 0.8 seconds or less as good, and a Largest Contentful Paint under 2.5 seconds as good. Those are what a real visitor measures, so they include network time that this loopback test leaves out. Use the origin number to find out how much of the budget your server spends.
- Origin TTFB well under 200 ms on every run: the server is not your problem. Go to Step 5.
- High on every run, even repeat requests: nothing is caching pages. Go to Step 2.
- Low on repeat requests, high on the uncached one: the work PHP and the database do per page is the cost. Go to Steps 3 and 4.
Step 2: Check that a page cache is answering
Without a page cache, WordPress runs PHP and queries the database for every anonymous visit. With one, Nginx returns a stored copy. If you set up a header as in the FastCGI cache guide, you can see which one you got:
curl -sI https://example.com/ | grep -i x-cache
On this server a cached page took about 5 ms and an uncached one about 54 ms, roughly a tenfold difference. In this setup any request with a query string bypasses the cache, which is a handy way to force an uncached request for testing. Do not stack two page-cache layers, such as an Nginx cache and a caching plugin. They fight over invalidation and make problems harder to trace.
Remember that the cache only helps anonymous visitors. Logged-in users and the admin area skip it by design, so a fast public site with a slow wp-admin is expected, not a fault.
Step 3: Look at what PHP is doing
When the uncached request is slow, the time is going into PHP or the database. Start with the cheap checks:
php -v
php-fpm8.4 -i | grep -E "^opcache\.(enable|memory_consumption|validate_timestamps) "
sudo -u www-data wp --path=/var/www/blog plugin list --status=active --fields=name,version
You want a supported PHP version, opcache.enable set to On for the FPM side, and a plugin list you can account for. Change the paths and the 8.4 to match your setup. This blog’s environment was WordPress 7.1.2 on PHP 8.4.20 with a custom theme and no active plugins, which is why its uncached time is small. It does not tell you what a plugin-heavy site costs, and measuring that on your own site is the point of this step.
To find the slow code, use the PHP-FPM slow log. It writes a backtrace for any request that runs longer than a threshold, and the file paths in it usually point straight at the plugin or theme responsible. Setting it up is covered in Step 6 of the PHP-FPM workers guide. If you suspect a plugin, test on a staging copy: deactivate plugins one at a time and repeat the Step 1 measurement after each change. Do that experiment on staging, not on production.
Step 4: Check the database
WordPress loads every option marked autoload on every request. If plugins have left large values there, every page pays for them. Check the size and row count:
W="sudo -u www-data wp --path=/var/www/blog"
P=$($W db prefix)
$W db query "SELECT COUNT(*) AS autoload_rows, ROUND(SUM(LENGTH(option_value))/1024) AS autoload_kb FROM ${P}options WHERE autoload IN ('yes','on','auto-on','auto');"
$W db query "SELECT option_name, LENGTH(option_value) AS bytes FROM ${P}options WHERE autoload IN ('yes','on','auto-on','auto') ORDER BY bytes DESC LIMIT 10;"
The second query lists the ten largest autoloaded options, which is where a plugin that stores a big blob usually shows up. For comparison, this blog measured:
| Measure | This blog |
|---|---|
| Autoloaded options | 179 rows, 45 KB |
| Option rows with “transient” in the name | 64 (this includes the paired timeout rows) |
| Post revisions | 19 |
| Database size | 6 MB |
WordPress Site Health starts flagging autoloaded data at roughly 800 KB, so a fresh blog like this one is nowhere near it. If yours is far larger, the biggest entries from the query tell you whose data it is. Expired transients are safe to remove, but take a backup before any cleanup:
sudo -u www-data wp --path=/var/www/blog db export /tmp/before-cleanup.sql
sudo -u www-data wp --path=/var/www/blog transient delete --expired
To undo, import the export with wp db import /tmp/before-cleanup.sql. Revisions can be limited later with WP_POST_REVISIONS in wp-config.php, but they rarely explain a slow site on their own. Sizing the database server itself is covered in the low-RAM MySQL guide.
Step 5: Check the page the browser receives
If the origin answers quickly and visitors still wait, the cost is in what the browser downloads and renders. Begin with compression. Compare the HTML with and without gzip:
curl -s --resolve example.com:443:127.0.0.1 -k https://example.com/ | wc -c
curl -s --resolve example.com:443:127.0.0.1 -k -H "Accept-Encoding: gzip" -o /dev/null -w "%{size_download}\n" https://example.com/
One article page here is 61,336 bytes of HTML and 14,438 bytes after gzip, about 76% smaller, and it references only 7 scripts, stylesheets and images in its HTML. Compression is set in Nginx with the gzip directives, not in an .htaccess file, which only Apache reads. To see your current settings:
sudo nginx -T 2>/dev/null | grep -E "^\s*gzip"
Then look for oversized images, which are the most common cause of a heavy page:
find /var/www/blog/wp-content/uploads -type f -size +300k -exec ls -lh {} \;
Anything large on that list is a candidate for resizing to its display size or converting to WebP. Finally, run the page through PageSpeed Insights and read the Largest Contentful Paint element it reports. That tells you which single file is holding the page back, which is more useful than the overall score.
Step 6: Rule out the server running out of resources
Slowness that comes and goes, or ends in 502 and 504 errors, usually means the server is short of memory or PHP workers rather than that WordPress is inefficient. Check:
free -h
uptime
sudo grep "max_children" /var/log/php8.4-fpm.log | tail
Heavy swap use or repeated max_children warnings point to memory pressure or undersized worker settings, and no page-level fix will cure those.
Which symptom points to which cause
| What you see | Likely layer | First check |
|---|---|---|
| Origin TTFB low, page still feels slow | Page weight and rendering | Step 5, PageSpeed Insights LCP element |
| Origin TTFB high on every request | No page cache | Step 2, X-Cache header |
| Fast when cached, slow when uncached | PHP or database work per page | Steps 3 and 4, slow log, autoload size |
| Slow at certain times, or 502/504 errors | Workers or memory | Step 6, FPM log, free -h |
| Only wp-admin is slow | Uncached PHP and plugins | Step 3, plugin test on staging |
Common mistakes
- Judging speed from your own browser. It has the site cached and your connection is probably better than your visitors’. Measure with curl and a speed-testing tool.
- Testing once. A single run can hit a cold cache or a cold worker. Take several.
- Chasing the PageSpeed score. Find the slowest layer first, then fix that.
- Adding a caching plugin on top of a server cache. Two layers make invalidation and debugging harder.
- Deleting database rows without a backup. Export first, every time.
- Copying Apache advice onto an Nginx server. Snippets for .htaccess do nothing on Nginx. Check which web server you run before applying a tutorial.
When tuning is not enough
If origin TTFB stays high after caching, the database is clean and PHP is current, the site has outgrown its server. Move to a bigger plan, split the database off, or choose managed WordPress hosting where the stack is already tuned and someone else watches it.
Related guides: adding swap space on an Ubuntu VPS and monitoring VPS bandwidth with vnStat. If something breaks, see fixing 502 Bad Gateway on Nginx and fixing “Error establishing a database connection”.
Related guides: tuning MariaDB for a low-RAM WordPress server, tuning MariaDB for high performance, caching WordPress with Nginx FastCGI, sizing PHP-FPM workers for limited RAM and diagnosing memory pressure on Linux.
Frequently asked questions
What is a good TTFB for WordPress?
Google’s guidance puts good TTFB at roughly 0.8 seconds or less, measured on real visitors. Measured on the server, a cached page should answer in a few milliseconds. This blog’s cached pages took about 5 ms. Uncached pages depend on your theme and plugins, so compare yours with your own cached figure.
Do more plugins always make WordPress slower?
Weight matters more than count. One heavy plugin can cost more than twenty light ones. Measure uncached TTFB before and after deactivating a plugin on staging, and use the PHP-FPM slow log to see where the time goes.
Is a caching plugin needed if Nginx already caches pages?
Usually not. A server-level cache answers before PHP starts. A second cache in a plugin adds invalidation rules to keep in sync for little gain.
Why is wp-admin slow when the public site is fast?
Public pages are served from the page cache, and the admin area never is. Every admin request runs PHP and the database, so a slow admin points at plugins, the database or PHP workers, not at caching.
The short version: split the delay with TTFB, confirm the cache, look at PHP and the database, then check the page itself, and fix the layer the numbers point to.