WordPress Scalability: What Breaks as Your Site Grows, and What to Do About It
Most WordPress sites are fast when they’re small. Then the catalogue goes from 2,000 products to 200,000, the orders table fills up, a dozen plugins have each left their settings in wp_options, and one day the shop page, the Products screen and the nightly import are all slow at once. Nothing changed in the code. The data grew, and the code was never written for that much of it.
That’s what this guide is about. WordPress scalability isn’t the same thing as WordPress speed. A fast site answers one request quickly. A scalable site keeps answering quickly as the tables get bigger and the visitors get more numerous, without the cost of each request growing with them.
This page is organised by what grows: posts and products, options, orders, traffic, admin work, background jobs and the outside services your site talks to. Each section says what breaks, why, what you can do about it for free, and which of my guides goes deeper. If you’d rather start from a symptom (“my site is slow, where do I look?”), the companion hub is the Performance Quick Start Guide. The two link to each other, and between them they cover most of what I’ve written on performance.
Is it a scaling problem or a speed problem?
Worth settling before you spend money on either.
A speed problem is roughly the same size whatever your data. A heavy theme, an unoptimised hero image, a page builder outputting 400 KB of markup. It was slow at 100 products and it’s slow at 100,000.
A scaling problem gets worse as something grows. The tell-tale signs:
- Uncached page generation time creeps up month by month with no code changes.
- wp-admin screens that list things (Products, Orders, Users, Media) get slower faster than the front end does.
- Imports that took ten minutes now take hours, or time out.
- The site is fine at 3am and falls over during a sale, or while the import runs.
- Query Monitor shows a modest number of queries, but a few of them take hundreds of milliseconds each.
The number to watch is page generation time, measured uncached: how long your server takes to build the page before anything is sent. A page cache hides it from some visitors but never removes it. I’ve written a full guide on measuring page generation time and bringing it down, and the free Query Monitor plugin will show it on every page (how to use Query Monitor to find the problem).
Every request that takes 3 seconds to build holds a PHP worker for those 3 seconds. Your server has a fixed number of workers, so slow page generation doesn’t just make one visitor wait, it caps how many visitors you can serve at once. That’s why scaling problems turn into outages under load rather than just slow pages.
If you want the plugin, page and milliseconds named for you rather than worked out by hand, that’s what Super Speedy Performance Analysis is for. It measures each plugin’s cost by switching it off for its own test requests only, so your visitors are never affected.
When posts and products grow
This is where most WordPress sites hit the wall first, and it’s the problem I built Scalability Pro for back in 2016.
The tables were designed for small sites
Every post, page, product and variation is a row in wp_posts. Every custom field, price, SKU, stock level and plugin setting attached to them is a row in wp_postmeta. Every category, tag and product attribute assignment is a row in wp_term_relationships. A store with 100,000 products can easily have millions of rows in wp_postmeta, because WooCommerce and every plugin that touches products stores its data there.
WordPress core indexes wp_postmeta on post_id and on the first 191 characters of meta_key. There’s no index on meta_value at all, and it’s a longtext column. So any query that filters or sorts by a meta value (price, stock status, a custom field, a featured flag) has to read every row with that key to find the ones it wants. On a small table that’s instant. On a table of millions of rows it’s a full scan on every page load.
The same pattern repeats elsewhere. Archive pages join wp_term_relationships against wp_posts to work out which products belong to a category, then sort the whole result before handing back the 20 you’ll see.
Counting is often the slowest part
To print “Page 1 of 5,000”, WordPress asks MySQL for every row that matches, not just the 20 on the page, using SQL_CALC_FOUND_ROWS. On a big catalogue that count can cost more than the rest of the page put together, and it runs on every archive page view. Replacing it with an estimate, a capped page count or plain Previous/Next links is one of the biggest wins available, at the cost of exact page totals. More in Remove SQL_CALC_FOUND_ROWS.
What to do
- Find the slow queries first. Sort Query Monitor’s queries by time and by rows. A handful of slow queries on a low total count means missing indexes or bad query shapes. Thousands of fast queries means a plugin doing something per product, which is a different fix. If you’re comfortable at the command line, the MySQL slow query log will catch the ones you didn’t think to look for: tracking down the origins of slow SQL and analysing slow MySQL 8 performance.
- Add the indexes WordPress is missing. You can do this by hand with
ALTER TABLEif you know which queries you’re targeting, andEXPLAINwill tell you whether MySQL uses them. Scalability Pro ships a tuned set coveringwp_posts,wp_postmeta,wp_term_relationships,wp_optionsand the WooCommerce lookup tables, and every one can be dropped again from the same screen: Custom Database Indexes. - Stop the expensive counts. See above, and Faster Archive Pages.
- For very large catalogues, take the category lookups off the relationship table. Once you’re into hundreds of thousands of posts, even an indexed join is expensive. Super Speedy Archives keeps a pre-computed table mapping posts to their terms, so an archive reads one indexed table instead of joining two big ones. Small sites won’t notice it.
- Replace the plugins that scan. Native WordPress search runs
LIKE '%term%'across your posts table, which can’t use an index however many you add. Many filter plugins count the products behind every filter option on every click. Neither gets fixed by Scalability Pro (it won’t rewrite another plugin’s queries from the outside), which is why Super Speedy Search and Super Speedy Filters exist as separate plugins.
The WooCommerce-specific version of all this, with the fixes in the order to try them, is in Why Are My WooCommerce Shop and Category Pages Slow?
When wp_options grows
wp_options doesn’t grow with your content, it grows with every plugin you’ve ever installed. Each one adds settings, and many add transients and caches. Uninstalling a plugin rarely removes them.
Every option marked as autoloaded is fetched and unserialised on every request, front end and admin, cached or not, through wp_load_alloptions(). A few hundred small ones are fine. A few large ones (a plugin storing a big array of stats, or a cache that should have been a transient) add a fixed cost to every single page. WordPress has a Site Health check for this since 6.6, and it warns once your autoloaded options pass 800,000 bytes by default.
An object cache doesn’t make this go away. Redis stores the autoload blob in memory, but PHP still has to fetch it and unserialise it on every request, so a bloated autoload just moves the cost. Clean it up first, then add the object cache.
The mirror image is a hot option that isn’t autoloaded, which costs one extra query per request on every page that reads it.
How to find and fix both, by hand or with Scalability Pro’s options scanner (which measures which options each page actually reads before proposing any change): Managing the wp_options Table and Autoload.
When orders and customers grow
A store taking a handful of orders a day and one taking thousands have very different databases, even with the same catalogue.
Turn on HPOS. WooCommerce’s High-Performance Order Storage moves orders out of wp_posts and wp_postmeta into their own tables. Apart from making order queries cheaper, it stops every order adding rows to the same tables your product queries run against. Check your plugins are compatible first (WooCommerce shows any that have declared otherwise on its Features screen).
Watch Action Scheduler. WooCommerce, Subscriptions and plenty of import and marketing plugins queue background work through Action Scheduler, and by default it keeps completed actions for 31 days. On a busy store those tables reach millions of rows, and they’re queried every time a job is scheduled or claimed, so the whole site pays for them. You can shorten the retention period with the action_scheduler_retention_period filter, or use Scalability Pro’s option and its buttons to clear an existing backlog: Optimise Action Scheduler.
Keep the checkout request short. Checkout can never be cached, and every plugin that hooks into placing an order runs while the customer waits: shipping and tax lookups, CRM syncs, emails sent inline, and cache-purge plugins reacting to order notes as if each order were a published page. That last one surprised me. On a test store running Nginx Helper with its default settings, one checkout made 30 outbound purge requests. On my own store, those purge requests were adding about 19 seconds to checkout. Scalability Pro’s Checkout Purge Shield stops them (with it on, that test checkout made none), and the full list of checkout causes and fixes is in Why Is My WooCommerce Checkout Slow?
Customer counts. The Users screen and the order list both count users, and core recounts every one of them to render a dropdown. On a store with a large customer base that’s a slow query on every load. Cache Author and User Counts covers it.
When traffic grows
More visitors is the growth everyone wants, and it’s where the page cache does the heavy lifting. Handle it at the server or CDN (Nginx FastCGI cache, LiteSpeed, Varnish or Cloudflare) rather than in a WordPress plugin, and don’t run two page caches at once. The Quick Start hub has the page caching detail, and if you’re behind Cloudflare, my Cloudflare setup guide for WooCommerce covers the cache rules.
The catch is that a page cache only serves anonymous visitors on pages that are identical for everyone. On a store, the visitors who bypass it are the ones you most want to keep happy:
- Logged-in customers. Most caching setups skip the cache entirely once a login cookie is present.
- Anyone with something in their basket. Same again, once the cart cookie appears.
- Visitors who see different prices. Geolocated currencies, tax display, member or trade pricing. If the price differs per visitor, the page usually isn’t cached at all.
- Cart, checkout and My Account, which should never be shared-cached.
As a store grows, that uncached share of traffic grows with it, and every one of those requests pays your full page generation time. This is why scaling the database matters even on a site with a good page cache: the uncached traffic is the traffic that buys things.
Two ways to bring more of it into the cache. For per-visitor prices, Super Speedy Ajax Prices caches the page with a placeholder and fills in each visitor’s prices and stock with one request (how it works). For logged-in customers and basket holders, I set it up myself as a fixed-price job: Cache Everything for Everyone. On my own store, a shopper with an item in their basket got cached pages with an 80 ms median time to first byte (3 requests, measured 13 August 2026). That’s my own site, measured from Athens: your number depends on your cache service and where your visitors are, which is why I measure yours before and after rather than quote you mine.
Two smaller things that matter during a rush. Plugins that bust the cache with random query strings or per-visitor output quietly turn cached pages back into PHP requests (avoid cache-busting plugins). And a missing favicon.ico sends a PHP request per visitor that no cache catches (create a static favicon).
When admin work grows
wp-admin is where a large site becomes unusable first, and it can never be page-cached. Every list screen counts, sorts and joins over the full tables, and the people using it are your own staff, all day.
The usual culprits are the per-status and per-author counts above every list table, the query that builds the Custom Fields dropdown, WooCommerce dashboard widgets and marketplace panels, and plugins phoning home while the page renders (more on that below).
Some of the measurements I’ve taken:
- On a site with about 2 million postmeta rows, one query that runs on wp-admin pages (the
SELECT DISTINCT meta_keythat core’s custom fields box and some themes run) went from about 1 second to under 4 ms once it had an index to use. - On a 1 million product test store, the wp-admin Products list went from about 4.3 seconds to about 0.65 seconds.
- On the same store, opening a product to edit went from about 1.4 seconds to about 0.2 seconds.
All three were with Scalability Pro’s settings applied, and they are measurements of those screens on those test sites, not promises about yours. The free starting point is the same as for the front end: open the slow screen with Query Monitor on and see which query dominates. The plugin’s side is in Faster wp-admin, Cache Post Counts and WooCommerce Admin Query Fixes.
When imports and background jobs grow
Imports, exports, price and stock syncs, sitemap generation and scheduled jobs all compete with your visitors for the same CPU, disk and database. As the catalogue grows, so does every batch job that touches it.
Imports. A slow import is rarely the importer’s parsing. It’s the database work per row: meta lookups against unindexed tables, term recounts after every product, image downloads and the thumbnails generated for every registered image size. Deferring term counting until the end of the run is usually the single biggest import win on a large catalogue (Defer Term Counting). The full treatment, covering every importer and not just mine, is the companion hub Fixing Slow WooCommerce Imports. In my 50,000-product update benchmark, WP All Import took 20 hours 11 minutes without Scalability Pro and 2 hours 23 minutes with it, on the same server and dataset. With Scalability Pro, Super Speedy Imports’ 50,000-product update ran in about 60 seconds, every time. Both benchmark pages list the hardware and the dataset.
WP-Cron. By default WordPress runs scheduled jobs on the back of page visits. On a busy site that means a visitor occasionally gets a slow page because they triggered a job, and on a cached site jobs can run late because no request reached PHP. Set DISABLE_WP_CRON in wp-config.php and run wp cron event run --due-now from a real system cron every minute instead. Your host’s control panel will usually do this for you.
Sitemaps. On-demand sitemaps that query every post at once are a batch job pretending to be a page, and the crawler is the one waiting. Super Speedy Sitemaps on large sites and Faster XML Sitemap Generation cover it.
When batch work and live traffic still fight after all that, the answer is to put them on different servers, which is the scaling-out section below.
When outside services grow
The more plugins you run, the more of them call out to licence servers, update checks, telemetry, shipping rates, tax services and CRMs. WordPress’s HTTP API is synchronous: if a call takes 5 seconds to time out, the page takes 5 seconds longer. You can’t cache your way out of it, and it gets worse with every plugin added.
Query Monitor’s HTTP API Calls panel lists every outbound request on a page with its time. Once you know which one it is, the fix is to move it out of the request (a background job), stop it (the plugin’s own settings, if it has one), or block it on the pages where it isn’t needed. Scalability Pro does the last of those with per-URL rules that can never touch the front end, checkout, payment hosts or the update screens: Block Slow Outbound HTTP Calls in wp-admin. If a plugin is slow everywhere but only needed on some pages, Unload Plugins safely by page is the other route.
Scaling up versus scaling out
Once the code is doing sensible work, the remaining question is hardware. There are two directions.
Scaling up means a bigger server: more CPU cores, more RAM, faster disks. It’s simple, it needs no changes to WordPress, and it’s where nearly every site should go first. But it only helps if the server is genuinely the limit. A query that scans 2 million rows scans 2 million rows on a 32-core box too, just slightly faster. Doubling the server to paper over a missing index is the most common (and most expensive) scaling mistake I see.
Scaling out means more servers. The usual first step is to separate batch work from live traffic: one server runs imports, exports and cron, another serves visitors, and the database replicates between them. After that come load-balanced web nodes, a database cluster and shared or replicated files. It removes the single point of failure and lets you take a node offline to upgrade it, but it brings real operational cost: file replication, cache invalidation across nodes, and making sure cron only runs in one place.
The order I’d do it in:
- Fix the queries and the autoload (sections above). Free or cheap, and it multiplies whatever hardware you have.
- Get the stack right: current PHP, in-memory object cache, MariaDB or MySQL 8, server-level page cache, NVMe disks.
- Scale up until a single server is no longer enough or no longer affordable.
- Move the database to its own server, and batch jobs to theirs.
- Build a cluster.
If you’re at step 5, my guides are here: Setting up a WordPress cluster for huge sites, then how to maintain and manage it, adding shared disk storage and using Unison instead of GlusterFS for file replication.
The stack underneath
Object cache
An in-memory object cache (Redis or Memcached) stores the results of repeated queries and computed values between requests, so the second visitor to a page doesn’t redo the first visitor’s work. On a growing site it’s no longer optional, and it’s the only kind of cache that helps logged-in users and wp-admin. Run Redis in memory with persistence switched off, give it a sensible maxmemory and an LFU eviction policy, and clean up wp_options before you switch it on. All of that is in Configuring Redis for WordPress and WooCommerce.
What Redis does differently on a WooCommerce store (sessions, carts, which cache groups to exclude, and where it stops helping) gets its own guide: Redis for WooCommerce.
Server stack
The detail of what a fast stack looks like (PHP-FPM, Nginx, MariaDB, Redis) and how to build one yourself is in Installing the fastest WordPress stack on Ubuntu 24.04 and the stack configuration and optimisation guide.
Hosting and control panels
Shared hosting is the one place none of this works, because you can’t control the database, can’t add server-level caching and can’t tell your slowness from a neighbour’s. Get off it first.
After that, my recommendation is to buy the control panel and the server separately. A panel lets you build on whichever provider suits your visitors’ location and your budget, and you can move providers without changing how you work. Whatever you choose, make sure it gives you staging, backups and cloning, because every fix on this page should be tried on a copy of the site first.
- Choosing WordPress Hosting for Performance
- Migrating huge WordPress sites reliably, for when you move
Where Scalability Pro fits, and where it doesn’t
I’ll be straight about this, because a plugin that’s bought for the wrong problem helps nobody.
Scalability Pro fixes WordPress’s own database work as the tables grow: the missing indexes, the expensive counts, the admin list screens, term recounting during imports, autoloaded options, Action Scheduler bloat, outbound calls in wp-admin and the checkout purge requests. It works on any post type, not just WooCommerce products. It doesn’t replace your page cache or your object cache, it makes the pages they can’t cover cheaper to build.
It won’t help much if:
- your site is small and Query Monitor shows no slow queries;
- your problem is thousands of fast queries from one plugin, or high PHP time with low SQL time, which points at code rather than the database;
- your slow page is search (that’s Super Speedy Search) or filtering (that’s Super Speedy Filters).
The honest self-test is Do I Need Scalability Pro?, and the mechanisms are explained in How Scalability Pro Works. If you’d rather someone else did the diagnosis and the fixing, I also take on performance work directly.
Frequently asked questions
Can WordPress handle a million products or posts?
Yes, if the database work is fixed. WordPress itself has no limit on rows, but its default indexes and queries assume a small site, so at that scale you need extra indexes, cheaper pagination counts, a cleaned-up wp_options table, an in-memory object cache and search and filtering that don’t scan tables. I run a 1 million product test store for exactly this reason.
Will a bigger server fix my scaling problem?
Only if the server is the limit. If one query reads millions of rows because it has no index, a bigger server makes it a little faster and leaves it just as wasteful. Find the slow queries with Query Monitor first, fix them, then decide whether you need more hardware.
Does a caching plugin make WordPress scalable?
It makes anonymous traffic to identical pages scalable, which is a lot of traffic. It does nothing for logged-in users, shoppers with a basket, visitors with personalised prices, checkout, wp-admin, imports or cron, and those all grow with your site. Treat the page cache as one layer, not the answer.
Is WooCommerce harder to scale than WordPress?
Yes, because more of its traffic can’t be cached (baskets, accounts, checkout), because product data lives in wp_postmeta and the attribute and lookup tables, and because orders, Action Scheduler and payment-related HTTP calls all add work that a blog never does. HPOS helps with the orders side.
When should I build a WordPress cluster?
When you’ve fixed the database work, scaled one server up as far as is sensible, and still need either more capacity or no single point of failure. Separating batch jobs from live traffic is usually the first step, and it’s often enough on its own.
Every guide in this cluster
Finding the bottleneck
- Performance Quick Start Guide: Solving WordPress Performance Problems (the symptom-led companion hub)
- WordPress page generation time: how to measure it and bring it down
- How to use Query Monitor to analyse WordPress performance problems
- How to do WordPress plugin performance analysis
- Super Speedy Performance Analysis: Quick Start Guide
- Super Speedy Performance Analysis: Understanding Your Results
Database and queries
- Tracking down the origins of slow SQL code in WordPress
- How to analyse slow MySQL 8 performance
- How to enable the slow query log and identify slow queries with PerconaDB
- Custom Database Indexes
- Remove SQL_CALC_FOUND_ROWS
- Alter the main query to use EXISTS rather than LEFT JOIN
- Finding Slow Queries
- Managing the wp_options Table and Autoload
Catalogue and archive pages
- Why Are My WooCommerce Shop and Category Pages Slow?
- Faster Archive Pages
- Super Speedy Archives Add-on
- Optimise Product Attributes Lookup
- Rebuilding a large search index with limited memory
Orders, checkout and background jobs
- Why Is My WooCommerce Checkout Slow?
- Checkout Purge Shield
- Optimise Action Scheduler
- Cache Author and User Counts
wp-admin
- Faster wp-admin
- Cache Post Counts
- WooCommerce Admin Query Fixes
- Block Slow Outbound HTTP Calls in wp-admin
- Unload Plugins safely by page
Imports and sitemaps
- Fixing Slow WooCommerce Imports (the imports hub)
- Faster Imports with Scalability Pro
- Defer Term Counting
- Diagnosing slow imports
- Database tuning for Super Speedy Imports
- Benchmark: WP All Import, 50,000-product update with Scalability Pro
- Benchmark: Super Speedy Imports, 50,000-product update with Scalability Pro
- Super Speedy Sitemaps: large sites and performance
- Faster XML Sitemap Generation
Caching and traffic
- Configuring Redis for WordPress and WooCommerce
- Redis for WooCommerce
- How It Works: Caching + AJAX Pricing
- Cloudflare Setup for WooCommerce and WordPress
- Avoid Cache-busting Plugins
- Create a static favicon.ico to avoid surplus PHP requests for rush traffic
Hosting, stack and scaling out
- Choosing WordPress Hosting for Performance
- Installing the fastest WordPress stack on Ubuntu 24.04 LTS
- WordPress stack configuration, customisation and optimisation guide
- How to: Setting up a WordPress cluster for huge sites
- How to maintain and manage a WordPress cluster
- Adding shared disk storage to a WordPress cluster
- How to use Unison instead of GlusterFS for faster file replication
- Migrating huge WordPress sites reliably
Scalability Pro
- Scalability Pro (product page)
- Do I Need Scalability Pro?
- How Scalability Pro Works
- Scalability Pro Quick Start Guide
- Measuring the Performance Win
- All Scalability Pro guides

Comments