WordPress Slow After 2 Years? Here's What's Actually Wrong

Your WordPress site was fast when you launched it. Now it drags. Here are the 5 most common causes of WordPress performance degradation and the SQL to diagnose each one.

Barry van Biljon
March 14, 2026
12 min read
WordPress Slow After 2 Years? Here's What's Actually Wrong
Back to Blog

Key Takeaways

  • WordPress doesn't slow down because of age. It slows down because of accumulated database bloat that nobody cleans up

  • The wp_options autoload data grows silently and loads on every single page request

  • Post revisions, orphaned postmeta, expired transients, and stacked cron jobs are the usual culprits

  • WooCommerce sites degrade faster because of Action Scheduler tables and session data in wp_options

  • You can diagnose all 5 problems with basic SQL queries before changing anything

Your site didn't start slow

When you launched your WordPress site, it was fast. Pages loaded in under a second. The admin panel was responsive. Everything worked the way it should.

Now it takes 3-5 seconds to load a page. The admin dashboard feels like it's running through mud. WooCommerce checkout makes customers wait. You've tried different caching plugins, you've disabled plugins one by one, maybe you've even switched hosts. Nothing made a real difference.

The problem isn't your plugins. It's probably not your hosting. And it's definitely not WordPress itself.

It's your database.

WordPress databases accumulate junk over time. Every plugin you install writes data. Every plugin you remove leaves data behind. Every product you delete, every post revision you save, every background task that runs. They all leave traces in your database tables that never get cleaned up.

After 2-3 years, these traces add up to megabytes of dead weight that WordPress loads on every single page request.

I've cleaned up databases on sites ranging from small business blogs to WooCommerce stores doing thousands of orders per month. The pattern is always the same: the site was fine at launch, got progressively slower, and nobody could figure out why.

Here are the 5 things that are almost certainly wrong, and how to check each one.


1. Your wp_options table is bloated with autoloaded data

This is the most common problem and the one with the biggest impact.

The wp_options table stores your site settings, plugin configurations, cached data, and a lot of things you've never heard of. A portion of this data is marked as "autoload," which means WordPress pulls it into memory on every single page request. Not just when it's needed. Every time.

On a fresh WordPress install, autoloaded data is about 300-500KB. On a 2-year-old site with a history of installed and removed plugins, I typically see 5-15MB. The worst I've seen was 40MB.

That's 40MB loaded into PHP memory before WordPress even starts building your page.

How to check

Connect to your database via SSH, phpMyAdmin, or any MySQL client and run:

SELECT
  ROUND(SUM(CASE WHEN autoload IN ('yes','on')
    THEN LENGTH(option_value) ELSE 0 END)/1024/1024, 2) as autoloaded_mb
FROM wp_options;

If the result is over 1MB, you have a problem. Over 5MB, it's serious.

What causes it

  • Plugins that write large serialized arrays to wp_options and set them to autoload
  • Expired transients (temporary cached data) that WordPress never garbage-collected
  • Settings from plugins you deactivated or deleted months ago
  • WooCommerce session data stored in wp_options instead of a dedicated table

How to fix it

I wrote a full step-by-step guide on this: How to Clean Your wp_options Table (The Right Way). It covers the exact SQL process I use across client sites, including what's safe to delete and what you should never touch.


2. Expired transients are piling up

Transients are WordPress's built-in caching system. Plugins use them to store temporary data (API responses, complex query results, external feed content) so they don't have to recalculate it on every page load. Each transient has an expiration time.

The problem: when a transient expires, WordPress doesn't delete it. It just sits there. WordPress only removes an expired transient when something specifically requests that exact transient. If nothing ever requests it again, it lives in your database forever.

On WooCommerce sites with marketing plugins, analytics integrations, and product feed generators, I regularly find 5,000-20,000 expired transients. Some sites have more. They're all sitting in wp_options, many of them autoloaded, all of them completely useless.

How to check

SELECT COUNT(*) as expired_transients
FROM wp_options a
JOIN wp_options b
  ON b.option_name = CONCAT('_transient_timeout_', SUBSTRING(a.option_name, 12))
WHERE a.option_name LIKE '_transient_%'
  AND a.option_name NOT LIKE '_transient_timeout_%'
  AND b.option_value < UNIX_TIMESTAMP();

Any number above zero means you have expired transients taking up space. Hundreds or thousands is common on older sites.

How to fix it

Expired transients are always safe to delete. WordPress regenerates them automatically when a plugin needs them again.

DELETE a, b FROM wp_options a
JOIN wp_options b
  ON b.option_name = CONCAT('_transient_timeout_', SUBSTRING(a.option_name, 12))
WHERE a.option_name LIKE '_transient_%'
  AND a.option_name NOT LIKE '_transient_timeout_%'
  AND b.option_value < UNIX_TIMESTAMP();

Or if you have WP-CLI access:

wp transient delete --expired

3. Post revisions are eating your wp_posts table

By default, WordPress saves unlimited revisions of every post and page. Every time you hit "Update," WordPress stores a complete copy of the content as a new row in wp_posts.

A blog post you've edited 50 times has 50 revision rows. A WooCommerce product description you've tweaked over the past year might have 100+ revisions. A page built with a page builder that auto-saves every 30 seconds can accumulate revisions in the hundreds.

I've seen sites where wp_posts had 200,000 rows and 80% of them were revisions. The actual content was maybe 500 posts and pages. The rest was dead copies.

This bloats your database size, slows down post queries, and makes database backups larger and slower.

How to check

SELECT
  post_type,
  COUNT(*) as count
FROM wp_posts
GROUP BY post_type
ORDER BY count DESC;

Look at the revision row. If it's 10x or more than your actual content rows, you have significant revision bloat.

How to fix it

First, limit future revisions by adding this to your wp-config.php:

define('WP_POST_REVISIONS', 5);

This keeps the 5 most recent revisions per post, which is enough for an "undo" safety net without the unlimited accumulation.

Then clean up existing excess revisions:

DELETE FROM wp_posts WHERE post_type = 'revision';

If you want to keep some revisions (say, the 5 most recent per post), that query is more complex. WP-CLI makes it simpler:

wp post delete $(wp post list --post_type='revision' --format=ids) --force

After deleting, optimize the table to reclaim disk space:

OPTIMIZE TABLE wp_posts;

4. Orphaned postmeta from deleted content

Every post, page, and product in WordPress has associated metadata stored in wp_postmeta. Product prices, custom fields, SEO settings, page builder layout data. It all goes in this table.

When you delete a post or product, WordPress removes the row from wp_posts. But the associated rows in wp_postmeta often stay behind. They reference a post ID that no longer exists. They're orphans.

On WooCommerce stores this is especially bad. A single product can have 30-100 postmeta rows (price, sale price, stock status, weight, dimensions, attributes, and various plugin metadata). Delete 500 products over 2 years and you could have 15,000-50,000 orphaned rows in wp_postmeta doing absolutely nothing except slowing down meta queries.

How to check

SELECT COUNT(*) as orphaned_meta
FROM wp_postmeta
WHERE post_id NOT IN (SELECT ID FROM wp_posts);

On a 3-year-old WooCommerce store I worked on recently, this returned 87,000 orphaned rows.

How to fix it

DELETE FROM wp_postmeta
WHERE post_id NOT IN (SELECT ID FROM wp_posts);

Then optimize:

OPTIMIZE TABLE wp_postmeta;

On large tables (1M+ rows), this DELETE can be slow because of the subquery. If it times out, you can batch it:

DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON pm.post_id = p.ID
WHERE p.ID IS NULL
LIMIT 10000;

Run it repeatedly until it returns 0 affected rows.


5. WooCommerce background tasks are stacking up

If you're running WooCommerce, there's a table called wp_actionscheduler_actions that stores every background task WooCommerce has ever scheduled. Order processing, email sending, webhook delivery, product sync, analytics calculations. It all goes through Action Scheduler.

The problem is that WooCommerce keeps completed and failed actions in this table by default. On a store that processes 50 orders a day, each order can generate 5-10 scheduled actions. After a year, you're looking at 100,000-200,000 rows in this table, most of them completed tasks that serve no purpose.

I've seen this table hit 500,000+ rows on busy stores. Queries against it slow down, which affects WooCommerce's ability to process new background tasks on time.

How to check

SELECT
  status,
  COUNT(*) as count
FROM wp_actionscheduler_actions
GROUP BY status
ORDER BY count DESC;

Look at the complete and failed rows. If they dwarf the pending count, you have cleanup to do.

How to fix it

Delete completed actions older than 30 days:

DELETE FROM wp_actionscheduler_actions
WHERE status = 'complete'
  AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 30 DAY);

Delete failed actions older than 7 days (after investigating why they failed):

DELETE FROM wp_actionscheduler_actions
WHERE status = 'failed'
  AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 7 DAY);

Then clean up the associated log entries:

DELETE FROM wp_actionscheduler_logs
WHERE action_id NOT IN (
  SELECT action_id FROM wp_actionscheduler_actions
);

And optimize both tables:

OPTIMIZE TABLE wp_actionscheduler_actions;
OPTIMIZE TABLE wp_actionscheduler_logs;

I wrote a dedicated deep-dive on this: Action Scheduler Table Bloat: The Hidden WordPress Performance Killer. The problem is bigger than most store owners realize.


The cron problem hiding behind all of this

There's a sixth issue that amplifies everything above. WordPress doesn't have a real cron system. It has wp-cron, which fires when someone visits your site. If nobody visits for 6 hours, no cron jobs run for 6 hours. When someone finally visits, every pending job fires at once.

This means transient cleanup, scheduled emails, WooCommerce background tasks, and plugin maintenance jobs all stack up and run simultaneously on a single page request. That one unlucky visitor gets a 10-second load time while WordPress catches up.

The fix is straightforward:

  1. Disable wp-cron in wp-config.php:
define('DISABLE_WP_CRON', true);
  1. Set up a real system cron job that runs every 5 minutes:
*/5 * * * * cd /var/www/your-site && wp cron event run --due-now > /dev/null 2>&1

This ensures background tasks run on schedule instead of ambushing random visitors.


Where to start

If you're staring at a slow WordPress site and wondering where to begin, here's the order I follow on every client site:

  1. Check wp_options autoload size. If it's over 1MB, clean it up first. This has the highest impact-to-effort ratio.
  2. Delete expired transients. Always safe. Takes 30 seconds.
  3. Count orphaned postmeta. If the number is in the thousands, clean it up.
  4. Check post revisions. Limit them and purge the excess.
  5. Check Action Scheduler (WooCommerce only). Clean completed tasks.
  6. Set up real cron. Replace wp-cron with a system cron job.

After all of this, run OPTIMIZE TABLE on wp_options, wp_posts, wp_postmeta, and (if applicable) wp_actionscheduler_actions.

Then measure. Compare your page load time, TTFB, and admin panel responsiveness to what they were before. On the sites I work on, I typically see TTFB improvements of 40-70% from database cleanup alone, before touching caching, CDN, or server configuration.


When database cleanup isn't enough

Sometimes you clean everything up and the site is still slow. That usually points to one of these:

  • Server-level bottleneck. Default PHP-FPM settings, insufficient memory allocation, or shared hosting resource limits. This is infrastructure work, not database work.
  • Plugin performance issues. Some plugins run expensive queries on every page load regardless of database health. Query Monitor is the best tool for identifying these.
  • No caching layer. Database cleanup reduces the cost of uncached requests, but you still need page caching (WP Rocket, WP Super Cache) and ideally an object cache (Redis) to handle traffic efficiently.
  • Architectural limits. WooCommerce stores that have outgrown the wp_postmeta model need to look at HPOS (High-Performance Order Storage) and product lookup tables. That's a bigger conversation.

The database cleanup is always step one because it has the highest return for the least effort. But it's not the only step.


Barry van Biljon

Written by

Barry van Biljon

Connect on LinkedIn

Full-stack developer specializing in high-performance web applications with React, Next.js, and WordPress.

Ready to Get Started?

Have questions about implementing these strategies? Our team is here to help you build high-performance web applications that drive results.

Frequently Asked Questions

Maybe, but probably not yet. Most hosting companies will recommend a bigger plan before looking at your database. Run the diagnostic queries in this post first. If your wp_options autoload data is over 2MB or your wp_postmeta table has hundreds of thousands of orphaned rows, no amount of server resources will fix what's fundamentally a data problem. Clean the database first, then reassess whether your hosting is actually the bottleneck.