September 4, 2026 · Vedanshu Jain
How to Load Test WooCommerce with k6: Script the Shopping Journey, Not the Homepage
Learn how to load test WooCommerce with k6 using a browse-to-checkout journey script, Cart-Token headers, and p95/p99 thresholds before a launch.
To load test WooCommerce with k6, script the shopping journey your customers actually take — browse, product page, add to cart, checkout — and drive it at a realistic arrival rate against a staging copy of your store. Measure p95 and p99 latency for cart and checkout, error rate, and orders per minute. Homepage benchmarks tell you almost nothing about whether checkout survives a launch.
Why a shopping-journey test beats hammering the homepage
Most “load tests” on WooCommerce stores fire a single URL thousands of times. The homepage is the easiest page to serve: fully cacheable, no session, answered by the CDN or page cache without waking PHP. A store can pass that test at 10,000 requests per second and still fall over when 300 people try to pay at once.
The requests that decide a launch are the ones the cache cannot help with. Add-to-cart creates a session and writes to the database. Checkout runs shipping, tax, coupons, stock checks, the gateway round trip, order creation, and emails — all uncached PHP competing for the same workers and database connections. That is the path 17% of US shoppers say they have abandoned because the “website had errors / crashed”, and it is the one your test must exercise.
Grafana’s k6 documentation separates smoke, average-load, stress, spike, soak, and breakpoint tests. For a launch you want an average-load run at expected peak and a breakpoint run that ramps until something breaks — both with the same journey script.
Staging first, production only with a plan
Test against a staging environment that mirrors production: same PHP worker count, database size, plugins, and cache configuration. A smaller server gives a pessimistic number; a stripped-down plugin list gives an optimistic one. Neither is useful. Three things to set up first:
- A test payment gateway. Enable Cash on Delivery or Cheque so orders complete without a real processor. Never load test through a live Stripe or PayPal account; you will trip fraud controls and may be suspended.
- Store API rate limiting off, or raised. WooCommerce’s optional Store API rate limiter is “disabled by default” (25 requests per 10 seconds when on). If your host or a security plugin enabled it, you will measure the limiter, not the store.
- Bot protection and WAF exceptions. Allow-list your load generator’s IPs so the CDN does not challenge or block it. Label runs by whether the edge cache was on; a run with it disabled is the harder, more honest test of the origin.
If staging cannot be made representative and you must test production, run at low-traffic hours with a dedicated test product, cap the rate below real peak, keep someone on the APM with a kill switch, and clean up the orders afterwards.
Cookies, nonces, and cart sessions
WooCommerce tracks a shopper’s cart in a session that is cookie-based. k6 handles this for you: it keeps “a cookie jar for each VU” and sends received cookies back automatically, so each virtual user behaves like one browser with one cart.
Writes need one more thing. Every POST to the Store API’s cart and checkout endpoints “will return an error unless a valid Nonce Token or Cart Token is provided.” You have two options:
- Nonce header. Make a GET to /wp-json/wc/store/v1/cart, read the Nonce response header, and send it back on subsequent requests. The docs note that “an updated Nonce header will be sent back — this needs to be stored and updated by the client”, so your script has to keep refreshing it.
- Cart-Token header. The same GET returns a Cart-Token header, and “when using a Cart-Token, a Nonce Token is not required.” This is the simpler path for a load script: fetch it once per iteration and reuse it.
Do not disable nonce checks with the woocommerce_store_api_disable_nonce_check filter to make testing easier; WooCommerce’s docs say it “should only be done on development sites where security is not important,” and a staging site that mirrors production should not carry it either.
A minimal k6 shopping-journey script
The script below uses the ramping-arrival-rate executor, which starts journeys at a fixed rate regardless of how slowly the store responds. With a fixed number of virtual users, a slow store quietly throttles your own test; an arrival-rate executor keeps pressure constant the way real traffic does. Thresholds use k6’s tagged-threshold syntax so cart and checkout are judged separately. Written for k6 2.x (k6 2.0 shipped in May 2026).
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE = __ENV.BASE_URL; // https://staging.example.com
const PRODUCT_ID = Number(__ENV.PRODUCT_ID);
const PRODUCT_URL = __ENV.PRODUCT_URL; // /product/test-item/
const API = `${BASE}/wp-json/wc/store/v1`;
export const options = {
scenarios: {
shoppers: {
executor: 'ramping-arrival-rate',
startRate: 20, timeUnit: '1m', // journeys per minute
preAllocatedVUs: 50, maxVUs: 500,
stages: [
{ target: 200, duration: '3m' },
{ target: 600, duration: '5m' },
{ target: 0, duration: '2m' },
],
},
},
thresholds: {
'http_req_duration{name:cart}': ['p(95)<800'],
'http_req_duration{name:checkout}': ['p(95)<1500', 'p(99)<3000'],
http_req_failed: ['rate<0.01'],
checks: ['rate>0.99'],
},
};
export default function () {
http.cookieJar().clear(BASE); // fresh shopper, fresh session
http.get(`${BASE}/`, { tags: { name: 'home' } });
http.get(`${BASE}${PRODUCT_URL}`, { tags: { name: 'product' } });
const cart = http.get(`${API}/cart`, { tags: { name: 'cart-get' } });
const headers = {
'Content-Type': 'application/json',
'Cart-Token': cart.headers['Cart-Token'],
};
const added = http.post(`${API}/cart/add-item`,
JSON.stringify({ id: PRODUCT_ID, quantity: 1 }),
{ headers, tags: { name: 'cart' } });
check(added, { 'item added': (r) => r.status < 300 });
sleep(2); // think time before paying
const address = {
first_name: 'Load', last_name: 'Test', address_1: '1 Test St',
city: 'Austin', state: 'TX', postcode: '78701', country: 'US',
email: `k6-${__VU}-${__ITER}@example.com`, phone: '5550100',
};
const order = http.post(`${API}/checkout`,
JSON.stringify({ billing_address: address, shipping_address: address,
payment_method: 'cod' }),
{ headers, tags: { name: 'checkout' } });
check(order, { 'order placed': (r) => r.status === 200 });
}The tags: { name } on each request lets thresholds and the summary group requests by step instead of by raw URL. Checks do not stop the test; they record a pass rate the checks threshold turns into a CI pass or fail. The checkout body follows the Store API checkout contract: billing_address, shipping_address, and payment_method are required. For variable products, pass a variation array to add-item; for multiple shipping methods, add a cart/select-shipping-rate step. Then match your real mix: most shoppers browse and leave, so run three or four browse-only journeys for every one that checks out.
What to measure
k6 reports a handful of built-in metrics. The ones that matter for a store are:
- p95 and p99 of http_req_duration for cart and checkout. Not the average: 400 ms average can hide a 9-second p99, and the p99 shopper is the one who leaves. Judge each step separately.
- http_req_failed. Above 1% at target load is a launch blocker. Note which step fails first — almost always add-to-cart or checkout, usually because PHP workers or database connections are exhausted.
- Orders per minute. Successful checkout responses per minute of the test — the number to compare against your launch forecast.
- Time to first byte. http_req_waiting isolates server time from transfer time and is the cleanest signal of origin load.
Watch the APM alongside k6 — worker saturation, slow queries, object cache hit rate. k6 says checkout is slow; the traces say why.
How to read the results
Plot latency against arrival rate. A healthy store shows a flat line that bends upward at some rate — that knee is your capacity. Latency climbing from the start means something is serializing requests (a lock, a single database connection, a synchronous third-party call). Errors before latency degrades mean a hard cap: rate limiting, a worker limit, or a connection pool.
Then read the failures. 5xx from checkout usually means PHP timeouts or the database refusing connections. 403 and 429 mean a security layer is blocking you, not the store failing. Store API 409 means the cart changed between requests — often a stock or coupon collision at high concurrency, a real bug worth finding before launch.
For large runs, k6 recommends setting discardResponseBodies and trimming checks so the load generator does not become the bottleneck. Run it from a region close to your customers, not next to your servers.
Running it safely before a launch
- Smoke test first with a handful of VUs to prove the script completes orders end to end.
- Baseline at expected peak — your forecast of journeys per minute in the launch hour — and record p95, p99, error rate, and orders per minute.
- Spike to 2–3× peak and hold. Launches never arrive at the forecast number.
- Break it. Ramp until errors exceed 1% or checkout p95 crosses budget. That is the capacity you tell the marketing team.
- Fix, re-run, and freeze. Every change to workers, caching, or plugins gets a re-run. Stop changing things a day before launch.
Keep the suite in the repository and run it from CI against staging on every release. WooCommerce ships updates “roughly every five weeks”, and the regressions that matter usually arrive with an update, not a traffic spike.
How Urumi handles this
Urumi runs a k6 shopping-journey suite of exactly this shape — browse, product, add to cart, checkout, at a ramping arrival rate — against every store on the platform before it goes live and before peak periods. Our published stress numbers come from that suite with the edge cache disabled, so the origin takes every request: 236 ms median cart response, 321 ms median checkout, and 7,861 orders completed in under two minutes.
The platform is built so those numbers hold as load grows: horizontal auto-scaling across multi-zone Google Cloud, a WooCommerce-aware cache that keeps cart and checkout correct while caching what it safely can, and a fully managed APM with traces, logs, and alerts so the “why” behind a slow checkout sits next to the k6 result. Through grüum’s peak weeks the platform absorbed 16× baseline load with 0 incidents. Already have a team or an agency? They ship faster with the grunt work covered — the load test, the APM, and the scaling are part of the managed WooCommerce platform, not a project someone has to own.
Frequently asked questions
How many virtual users do I need to load test a WooCommerce store?
Think in journeys per minute rather than virtual users. Estimate peak sessions per hour from analytics, convert to journeys per minute, and let a ramping-arrival-rate executor allocate the VUs it needs. A VU-based test slows itself down when the store slows down, hiding the problem you are looking for.
Can I load test WooCommerce checkout without a real payment gateway?
Yes. Enable Cash on Delivery or Cheque on staging and pass that gateway’s ID as payment_method to the Store API checkout endpoint. The order pipeline — shipping, tax, stock, order creation, emails — runs in full; only the processor round trip is skipped.
Should I test with the CDN or edge cache enabled?
Do both and label them. Edge cache on shows what shoppers experience; edge cache off shows what the origin can take when caches are bypassed, as they are on cart and checkout. Publish the harder number.
What is a good p95 for WooCommerce checkout under load?
Under 1 second for add-to-cart and under 1.5 seconds for checkout at expected peak, with errors below 1%, is a defensible budget. Deloitte’s retail research found a 0.1 second speed improvement lifted retail conversions by 8.4%, so tighter budgets pay for themselves.
If the knee arrives before your forecast, read why WooCommerce slows down under load and how many PHP workers WooCommerce needs before buying a bigger server.
Last reviewed September 2026. Competitor details come from their public pages on the dates linked; check them before you buy.
Built by the people who built WooCommerce core.
We built WooCommerce core at Automattic — the parts that matter in production: performance, payments, reliability. Earlier, engineering at HackerRank (Y Combinator) through its enterprise scale-up. Naman led Payments and WooCommerce releases to 4.5M merchants; Vedanshu led HPOS, Taxes, and Shipping. Run by AI, overseen by the people who built WooCommerce core.
Grow your store's revenue on Urumi.
The AI platform D2C brands use to grow revenue — built by the people who built WooCommerce core.
See the WooCommerce platform · Start your WooCommerce store on Urumi · Talk to the founders
Agent live · 99.99% uptime · shipping today.