How-to

How to Load Test a WordPress or WooCommerce Site

Updated October 27, 2026•10 min read•WordPress•PHP

Written by

Reviewed by the LoadTester editorial team. Review process: see the editorial policy.

Published
2026-10-27
Last reviewed
2026-10-27
Author
Kristian Razum
How to Load Test a WordPress or WooCommerce Site: LoadTester illustration

To load test WordPress, test two things separately: cached anonymous pages (which measure your page cache or CDN) and uncached paths such as logged-in pages, search, admin-ajax.php and the WooCommerce cart and checkout (which measure PHP-FPM and MySQL). The uncached number is your real capacity, and it is usually limited by pm.max_children, which you should size from memory: available RAM divided by average PHP worker size. Get your host's permission first and verify the domain before running anything.

Why most WordPress load tests mislead

A typical first test hits the home page with 500 virtual users, gets a 20 ms response from a full-page cache, and declares the site ready for anything. Then a sale starts, customers add products to their carts, and the site returns 502 Bad Gateway at a fraction of that load.

The reason is that WordPress has two very different performance profiles:

PathServed byTypical cost per requestCapacity is limited by
Anonymous page, cache hitCDN, Varnish, nginx FastCGI cache, or a caching plugin's static fileUnder 1 ms of server CPUBandwidth, CDN, nginx
Anonymous page, cache missPHP-FPM + MySQL50–500 ms of PHP, 20–200 queriesPHP workers, CPU, DB
Logged-in user, any pagePHP-FPM + MySQL (page cache bypassed)Same as a miss, often morePHP workers, CPU, DB
WooCommerce cart, checkout, my-accountPHP-FPM + MySQL, never page-cachedHeavier: sessions, totals, shipping, taxPHP workers, DB writes and locks
admin-ajax.php, REST API, ?wc-ajax=PHP-FPM, usually uncachedFull WordPress bootstrap per callPHP workers

The cached number tells you your CDN works. The uncached number tells you when your site falls over. You need both, measured separately, then a realistic mix.

What typically breaks, and the error you will see

PHP-FPM runs out of workers

Each in-flight uncached request occupies one PHP-FPM worker for its whole duration. When all are busy, nginx queues and eventually times out. In the PHP-FPM log you will see:

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

And in the nginx error log, depending on the failure:

connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable)
upstream timed out (110: Connection timed out) while reading response header from upstream

The default www.conf on Debian and Ubuntu ships with pm.max_children = 5, which is far too low for most production servers. The first symptom is often 502s; our guide to 502 Bad Gateway under load covers the nginx side.

MySQL connections or slow queries

Each PHP worker opens its own MySQL connection. MySQL's default max_connections is 151; WordPress reports the failure as Error establishing a database connection. More often the problem is not the connection count but slow queries: wp_options with megabytes of autoloaded data, unindexed wp_postmeta lookups from plugins, or WooCommerce order queries on a large table. Enable the slow query log during the test (long_query_time = 0.5).

admin-ajax.php

Many plugins and themes call /wp-admin/admin-ajax.php on every page view (view counters, popups, live search), and the Heartbeat API calls it periodically for logged-in users. Each call loads all of WordPress and is never page-cached. Check the browser network tab on your key pages; every admin-ajax.php call you see belongs in the test script.

WooCommerce cart fragments

The cart fragments script requests /?wc-ajax=get_refreshed_fragments to update the mini-cart. It is a POST to PHP, uncached, and on themes that load it everywhere it fires on page loads for every visitor, which means a fully cached site still sends one PHP request per page view. Recent WooCommerce versions (7.8 and later) no longer load it on every page by default, but many themes and the classic cart widget still enqueue it. Check your own pages rather than assuming.

Cookies that switch off the page cache

Caching plugins and hosts bypass the page cache when they see wordpress_logged_in_*, woocommerce_items_in_cart, woocommerce_cart_hash or wp_woocommerce_session_* cookies. After a customer adds one item to their cart, every page they view for the rest of the session is uncached. A sale that drives lots of add-to-carts therefore shifts traffic from cache to PHP over the course of the event.

WP-Cron

By default WP-Cron is triggered by visitor requests. Under load, scheduled jobs (WooCommerce scheduled actions, backups, email digests) can start mid-test and skew results. Disable it and run it from system cron:

// wp-config.php
define( 'DISABLE_WP_CRON', true );

# crontab -e (as the web user)
*/2 * * * * cd /var/www/html && wp cron event run --due-now --quiet

Sizing pm.max_children from memory

Do not raise pm.max_children to a large round number. If the workers do not fit in RAM, the server swaps or the OOM killer starts terminating processes, which is worse than queuing. Size it from memory:

pm.max_children = (RAM available for PHP) / (average PHP-FPM worker RSS)

Measure the average worker size on the live server after it has served real traffic for a while (workers grow as they handle requests):

ps --no-headers -o rss -C php-fpm8.2 | awk '{ s+=$1; n++ } END { printf "%d workers, avg %.0f MB\n", n, s/n/1024 }'

Worked example for an 8 GB server running everything on one box:

Total RAM                          8,192 MB
- OS, nginx, monitoring           -1,024 MB
- MySQL (innodb_buffer_pool etc.) -2,560 MB
- Redis object cache                -512 MB
= Available for PHP                4,096 MB

Average worker RSS (measured)         85 MB
4,096 / 85                          = 48  ->  set pm.max_children = 45 (keep ~10% margin)

Then check whether 45 workers can deliver the throughput you need. By Little's Law, uncached throughput = workers / average response time. With 45 workers and 300 ms per uncached request, the worker ceiling is about 45 / 0.3 = 150 uncached RPS, provided you have the CPU. PHP rendering is largely CPU-bound: if 200 ms of each 300 ms request is spent on CPU and you have 4 vCPUs, the CPU ceiling is about 4 / 0.2 = 20 RPS no matter how many workers you configure. More workers than cores helps only when workers spend time waiting on MySQL or HTTP calls. The load test tells you which of the two limits you hit.

A reasonable pool configuration for this example:

; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 45
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 16
pm.max_requests = 500          ; recycle workers to cap memory growth from leaky plugins
request_slowlog_timeout = 5s
slowlog = /var/log/php8.2-fpm-slow.log

The slowlog is the most useful file during a WordPress load test: it prints a PHP stack trace for any request slower than 5 seconds, which usually points straight at the plugin responsible.

Before you test: host permission and domain verification

  • Read your host's terms. Many managed and shared WordPress hosts prohibit load testing without permission, and shared hosting means your test affects other customers. Open a ticket with the date, window, expected peak RPS and source IPs.
  • Allowlist the load generators. Security plugins (Wordfence and similar), host firewalls, and Cloudflare will rate-limit or challenge a test that comes from a few IPs. LoadTester publishes its worker IPs at https://app.loadtester.org/api/ips for this; see load testing behind Cloudflare for WAF rules.
  • Verify the domain. Hosted tools require proof you control the site. In LoadTester that is a DNS TXT record named _loadtester-verify or a file at /.well-known/loadtester-verify.txt. On WordPress, the file route works as long as your web server serves /.well-known/ as static files rather than routing it to index.php.
  • Prepare test data. Use a staging copy with production-sized tables when possible. If you must test production, disable order emails for test accounts, use a test payment gateway, and plan how you will delete test orders.

Test cached and uncached paths separately

Run 1: cached anonymous pages

Request the home page, three category pages, and ten product pages with no cookies. Confirm hits from the response headers before trusting the numbers: x-cache: HIT (Varnish), x-fastcgi-cache: HIT (if you add that header in nginx), cf-cache-status: HIT (Cloudflare), or your plugin's HTML comment. This run should scale to high RPS with flat latency; if it does not, the bottleneck is in front of PHP.

Run 2: forced cache misses

Add a unique query string to each request (?lt=${random}) so most page caches treat it as a new URL. Some caches ignore unknown parameters or strip them, so check the headers show a MISS. Ramp slowly: this run finds your PHP ceiling, and on a small server it may be under 50 RPS.

Run 3: session paths

A multi-step scenario that behaves like a shopper: view product, POST /?wc-ajax=add_to_cart with product_id and quantity, load /cart/, load /checkout/, call /?wc-ajax=update_order_review. Keep the cookie jar per virtual user so the session cookie carries over. Stop before payment or use a test gateway. The checkout load testing guide covers scripting checkout without creating real orders.

A realistic scenario mix

After the three isolation runs, combine them. A starting point for a WooCommerce store, to adjust from your analytics:

ScenarioShare of sessionsCache behavior
Browse: home, category, 2–3 products60%Mostly cached
Search: /?s=term or product search12%Uncached
Add to cart, view cart, keep browsing18%Uncached after add-to-cart
Checkout to order review6%Uncached, write-heavy
Logged-in customer: my-account, orders4%Uncached

Add think time between steps (a few seconds) so the session cookies and cart state behave as they do for real users. Ramp to your target, hold for 15–30 minutes, and watch PHP-FPM active processes, MySQL threads running, and CPU alongside p95. A short hold hides problems such as object cache eviction or a growing wp_options autoload. For store-wide planning around sale days, the ecommerce load testing guide covers forecasting.

Fixes that usually help, in order

  1. Persistent object cache (Redis or Memcached) so repeated option and query lookups skip MySQL.
  2. Clean up autoloaded options: SELECT SUM(LENGTH(option_value)) FROM wp_options WHERE autoload IN ('yes','on'); If that is over about 1 MB, find the plugin responsible.
  3. Remove or defer plugins that call admin-ajax.php on every page view, and reduce Heartbeat frequency for logged-in users.
  4. Enable WooCommerce High-Performance Order Storage (HPOS) if you are still on post-meta order storage.
  5. Size pm.max_children from memory as shown above, then add CPU or a second app server if CPU is the limit.
  6. Cache at the edge with cookie-aware rules so anonymous visitors never touch PHP.
Next step

Get written permission from your host, verify the domain, then run the forced-miss test first. The uncached RPS at which p95 crosses your threshold is the number to plan around; everything cached is a bonus on top of it.

FAQ

Is it allowed to load test a site on managed WordPress hosting?

Check your host's terms of service first. Many managed and shared WordPress hosts prohibit load or stress testing without prior permission, and their DDoS protection may block or rate-limit the test. Open a ticket with the planned date, duration, peak RPS and source IPs, and wait for written approval.

Why does my WordPress site handle thousands of RPS in a test but fall over with real users?

Your test probably only requested cached anonymous pages. Real users log in, add to cart, search, and trigger admin-ajax.php calls, all of which bypass the page cache and run PHP plus MySQL. Test those uncached paths separately and include them in the mix.

What does 'server reached pm.max_children setting' mean?

PHP-FPM has started every worker process it is allowed to and new requests are queuing behind them. Either the workers are slow (often waiting on MySQL or an external API) or there are too few of them. Size pm.max_children from available memory divided by average worker memory, and fix slow queries before adding workers.

Should I disable WP-Cron during a load test?

Yes, and in production too for busy sites. Set DISABLE_WP_CRON to true in wp-config.php and run wp-cron.php from a system cron every minute or few minutes. Otherwise cron checks piggyback on visitor requests and scheduled jobs can fire in the middle of your test.

Do caching plugins work for WooCommerce cart and checkout pages?

No, and they should not. Cart, checkout and my-account pages are user-specific and are excluded from page caching by every mainstream caching plugin and host. That is why those pages, plus anything that sets WooCommerce session cookies, need their own load test with realistic PHP and database capacity.

Run this as a real load testFree plan: 10 VUs, 50 RPS, live p95/p99 and error charts, no credit card. Starter $19/mo (250 VUs) or Pro $49/mo (2,000 VUs) with a 7-day free trial when you need more.
Start freeCompare plans