WooCommerce 10.6.2 Hetzner 8 vCPU Dedicated PHP 8.3 April 2026

500 Concurrent Users on WooCommerce: A Store API vs Classic Checkout Load Test

Two identical WooCommerce installs on the same Hetzner box. Same products, same PHP, same Redis.

One uses classic wc-ajax endpoints for cart and the shortcode checkout. The other uses the Store API REST layer for cart, coupons, and the block-based checkout. I hit both with k6 across the full purchase flow: add to cart, coupon operations, checkout, and frontend page loads under up to 500 concurrent users.

Classic wins on raw checkout speed. Store API wins on consistency under load. Neither won outright.

ServerHetzner CCX33 - 8 dedicated AMD EPYC vCPU / 32GB RAM / 240GB NVMe
Web ServerOpenLiteSpeed 1.8.5
PHPLSPHP 8.3.30 (60 workers, OPcache 256MB)
DatabaseMariaDB 10.11 (4GB buffer pool, separate DB per instance)
Object CacheRedis 7.0 (isolated DB indices per instance)
WordPress6.9.4
WooCommerce10.6.2
ThemeGeneratePress 3.6.1 (same on both, no child theme)
Plugins (both)WC Subscriptions 8.6.0 / WC Payments 10.6.0 / Redis Cache 2.7.0
Page CachingDisabled for API tests / LSCache tested separately
Products9 products: 3 variable (7 variations), 3 simple, 3 subscription
Classic Instance[woocommerce_checkout] shortcode
Store API Instancewp:woocommerce/checkout block
Testing Toolsk6 1.7.1 (Grafana) / Lighthouse CI 0.15.1 / Puppeteer 24.x

Why This Test Exists

This started with a client project. A high-traffic store was planning a rebuild, and the team split on whether to go all-in on blocks and the Store API or stay with the classic wc-ajax/shortcode stack.

WooCommerce has been pushing block checkout hard since version 8.3. It's the default for new installs in WC 10.x, and the classic shortcode may eventually be deprecated. Meanwhile, 87% of stores still run classic checkout. The migration pitch is better extensibility and a "modern" architecture, but nobody was publishing real load test numbers comparing the two approaches on the same hardware.

I rented a Hetzner CCX33 (~€48/month), stood up two identical WordPress instances with the same catalog and plugins, and ran the same k6 scripts against both. Every test ran at least 3 times. The numbers below are medians across runs.

WooCommerce 10.5+ ships several performance features disabled by default. I turned them all on: HPOS datastore caching, REST API caching, product instance caching, order attribution off. Without them, Store API was 9% slower across the board. With them, the gap narrowed to single digits.

Cart Operations: Store API Handles the Crowd Better

At one user, both handle add-to-cart in 80ms. At 50 concurrent users, still neck and neck. Past 200 VUs, Store API pulls ahead. Not by much on the median, but the tail tells a different story.

Both approaches load the WordPress stack. The difference is what happens after:

  • Classic /?wc-ajax=add_to_cart runs WooCommerce's legacy session handling and returns HTML fragments for the mini-cart widget.
  • Store API returns a structured JSON response and avoids the template rendering overhead, though it still bootstraps the full REST API layer.
Add to Cart -Median Response Time by Concurrency
Classic (wc-ajax) Store API
Coupon Operations -P95 Tail Latency at 500 VU
At 500 VUs, classic's coupon P95 hit 3,546ms. Store API: 692ms. 5x more consistent. Classic's coupon validation locks the WooCommerce session, and under load those locks stack up. Your customers see a spinner for 10 seconds while everyone else gets instant responses.

Place Order: Classic's Server Response Is Faster Under Load

Classic's Place Order endpoint responds faster at every load level I tested. By 22% at one user. By 127% at 500. This is the server-side response time for the final "submit" click only, not the full checkout experience (which favors Store API by ~8% due to faster update waits).

Classic reads POST fields from a form and returns a JSON redirect with the order ID. One function call wrapped in validation. Store API parses typed address objects, validates them against a JSON schema, runs payment through a structured pipeline, serializes the complete order response, and returns the full cart state. More work per request. Better API design. Slower response.

Checkout -Median Response Time by Concurrency
LoadClassicStore APIGapTimeouts (C / SA)
1 user185ms225ms+22%0% / 0%
50 users1,642ms2,673ms+63%0% / 0%
200 users2,107ms4,706ms+123%1.6% / 5.3%
500 users2,643ms5,986ms+127%4.4% / 16.1%

The Fidgety Shopper: Where Store API Earns Its Keep

Isolated benchmarks don't reflect real shopping behavior. Customers browse, add three things, remove one, change a quantity, try a coupon code that doesn't work, try another one, then check out.

I wrote a k6 script that does exactly this: ~12 cart operations per session. Adds, removes, quantity changes, coupon swaps. Then 200 of these fidgety shoppers hit the server at once.

Cart Operation Latency Distribution -200 Concurrent Shoppers
The P95 is what makes this test interesting. Classic's median is better (94ms vs 159ms), but 5% of classic users waited over 10 seconds for a single cart operation. Store API's worst-case was 268ms. If you're running a subscription store where customers modify their carts regularly, that 38x difference in tail latency is the difference between "add to cart" feeling instant and customers wondering if the site is broken.

Frontend Performance: Lighthouse TTI Is Misleading

I ran Lighthouse CI, then measured Real TTI with performance.now() across three CPU throttle levels. The gap between the two metrics is significant.

Lighthouse TTI is inflated by 84% for Store API. Two compounding factors:
  • 5-second quiet window: Lighthouse waits for 5 seconds of idle after the last long task before declaring "interactive." React hydration finishes, the page is responsive, but Lighthouse keeps the clock running. Classic's jQuery has no hydration step, so it isn't affected.
  • Simulated throttling inflates code-split bundles: Chrome doesn't actually slow the CPU. It adds artificial idle pauses after each task. Store API's 66 chunks each get a gap (vs Classic's ~15 scripts), inflating the timeline 3-4x more. Real TTI via Emulation.setCPUThrottlingRate: 4,243ms. Lighthouse reported: 7,804ms.
Real TTI vs Lighthouse TTI (first-time, without WC Payments)
ThrottleMetricClassicStore APIGap
1x (desktop)Real TTI384ms968ms2.5x
1xLighthouse TTI2,248ms2,222msTied
2x (mid-range phone)Real TTI400ms3,885ms9.7x
4x (low-end phone)Real TTI2,486ms4,243ms1.7x
4xLighthouse TTI4,465ms7,804ms1.75x
Lighthouse metrics (first-time, with WC Payments)

Warm cache eliminates the difference. At every throttle level, both hit Real TTI under 700ms. Zero long tasks at 1x and 2x. The architecture gap vanishes.

ThrottleClassic (warm)Store API (warm)Gap
1x (desktop)~310ms~320msTied
2x (mid-range)~400ms~450msTied
4x (low-end)565ms673ms+19%
Lighthouse metrics (returning, with WC Payments)
Full comparison: Lighthouse + Real TTI (without WC Payments)
First-time (4x throttle)Returning (4x throttle)
MetricClassicStore APIClassicStore API
Real TTI2,486ms4,243ms565ms673ms
Lighthouse TTI4,465ms7,804ms~1,000ms~1,000ms
TBT2ms83ms3ms66ms
Long tasks0211
Size75 KB681 KB38 KB63 KB
Use Real TTI, not Lighthouse TTI, for Store API comparisons. Lighthouse inflates Store API's TTI by 3-5 seconds because of the quiet window algorithm. The real gap on a mid-range phone is ~3.5 seconds on first visit (real, caused by React hydration), but near zero on return visits at any throttle level.

Mobile Networks: It Depends on Your Customers' Connections

Store API downloads 2x more data on first visit, but its assets split into ~120 small chunks fetchable in parallel over HTTP/2. On fast connections, parallelism beats payload size. On 3G, the extra bytes win.

Checkout Page Load -Mobile Network (first-time)
NetworkClassicStore APIGap
No throttle1,095ms1,120msTied
4G1,657ms2,288ms+38%
3G3,498ms4,810ms+37%

Both transfer ~40-65KB from the server. The rest loads from cache. The 3G penalty drops from +37% to +11%.

Checkout Page Load -Mobile Network (returning)
NetworkClassicStore APIGap
No throttle870ms832ms-4% (SA wins)
4G911ms908msTied
3G1,147ms1,274ms+11%
Mobile Network Page Load: First-Time vs Returning
First-time visitorReturning visitor
NetworkClassicStore APIClassicStore API
No throttle1,095ms1,120ms870ms832ms
4G1,657ms2,288ms911ms908ms
3G3,498ms4,810ms1,147ms1,274ms
Cache benefit (no throttle)-21%-26%
For returning visitors on fast connections, Store API is 4% faster. On 4G, tied. On 3G, Classic still wins but the gap shrinks from 37% to 11%. The payload difference mostly vanishes when the browser cache is warm.

The Actual Checkout Experience: Store API Feels Faster

I used Puppeteer to simulate what a customer actually does: navigate to checkout, fill every billing field, wait for the totals to recalculate. End-to-end, from page load to "ready to place order."

Checkout Interaction Breakdown (first-time)
MetricClassicStore API
Checkout page load1,032ms1,103ms
Update wait after fields2,004ms1,005ms
Total interaction time6,881ms6,377ms

Field fill, update wait, and server calls are server-side work, unchanged by caching. Only page load benefits from warm cache.

Checkout Interaction Breakdown (returning)
MetricClassicStore API
Checkout page load1,169ms819ms
Update wait after fields2,007ms1,004ms
Total interaction time6,925ms6,331ms
Checkout Interaction: First-Time vs Returning
First-time visitorReturning visitor
MetricClassicStore APIClassicStore API
Page load1,032ms1,103ms1,169ms819ms
Update wait2,004ms1,005ms2,007ms1,004ms
Total interaction6,881ms6,377ms6,925ms6,331ms
Server calls1515
The interaction metrics (field fill, update wait, server calls) are unchanged by caching. Only page load benefits from warm cache. Store API wins on total interaction time by ~8% on both visits. The update wait advantage (2x faster) outweighs the first-visit page load penalty. Classic makes 1 server call during fill, Store API makes 5, but those 5 finish faster total.

Caching: Redis Narrows the Gap from 13% to 3.4%

The checkout gap between Classic and Store API shifts depending on your caching stack. I tested four configurations at 50 concurrent users.

Place Order response time & gap by caching config (50 VU)
Redis narrows the checkout gap because Store API makes more internal function calls that benefit from object caching. WooCommerce's wc_get_product(), session reads, and schema validation all hit the object cache. Classic uses fewer of these calls, so it benefits less. With Redis + LSCache combined, the gap drops from 13% to 3.4%.

With Real Stripe Payments: The Gap Nearly Disappears

All the checkout data above used Cash on Delivery to isolate WooCommerce's processing from payment gateway latency. But real stores use Stripe. I ran the full checkout flow with Stripe's Payment Element on both instances.

Full Checkout with Stripe (page load to order confirmed)
StepClassicStore APIGap
Page load3,218ms2,695msSA 16% faster (Stripe iframe loads in parallel with block chunks)
Time to interactive8ms1,509msReact hydration
Fill billing fields3,428ms3,837msSimilar
Fill Stripe card2,747ms2,746msIdentical
Submit to confirm5,034ms5,293msClassic 5% faster
Total16,435ms17,180ms+4.7%
Stripe adds ~5 seconds to "Place Order" on both architectures (PaymentIntent creation, confirmation, webhook). With COD, Classic's checkout was 22% faster. With Stripe, only 5%. The payment gateway latency dominates, making WooCommerce's ~1 second processing overhead marginal.

Heavy Data: 500K Orders Change the Math

All previous tests ran against a clean store with 9 products and minimal order history. Real stores have large wp_woocommerce_order_itemmeta, wp_usermeta, and coupon tables that change the performance picture. I loaded 500K orders, 100K users, 2M usermeta rows, 200 products, and up to 10K coupons with 500K _used_by entries.

Checkout: Empty Store vs Heavy Data (500K orders)
LoadClassic (empty)Classic (heavy)ImpactSA (empty)SA (heavy)Impact
1 VU185ms219ms+18%225ms266ms+18%
50 VU1,642ms1,986ms+21%2,673ms2,559ms-4%
200 VU2,107ms2,356ms+12%4,706ms3,303ms-30%
500 VU2,643ms5,424ms+105%5,986ms7,128ms+19%
The checkout gap between Classic and Store API halves with heavy data: from +123% to +40% at 200 VU, from +126% to +31% at 500 VU. Store API checkout actually gets 30% faster at 200 VU with heavy data because the warm object cache benefits its internal wc_get_product() and schema validation calls.
Add to Cart: Empty Store vs Heavy Data
LoadClassic (empty)Classic (heavy)ImpactSA (empty)SA (heavy)Impact
1 VU80ms88ms+10%80ms93ms+16%
50 VU243ms320ms+32%252ms374ms+48%
500 VU610ms5,275ms+765%578ms1,856ms+221%
At 500 VU with heavy data, Classic cart collapses: 5.2 seconds median, 60-second P95. Store API degrades to 1.8 seconds but stays functional. Classic's wp_posts table at 500K rows + session table locks destroy performance. Store API's stateless approach and wc_product_meta_lookup table handle the load.

Why Heavy Data Changes the Picture

  • Classic's session table becomes a bottleneck. wp_woocommerce_sessions with 500K orders generates more active sessions and row-level lock contention. Store API is stateless.
  • Store API benefits more from object caching. With 100K users and 500K orders, Redis stays warm with frequently-accessed data. Store API makes more wp_cache_get() calls, so it benefits disproportionately.
  • The wp_posts table at 500K rows hurts Classic cart. Classic's wc-ajax endpoints query wp_posts for product lookups. Store API uses the smaller, better-indexed wc_product_meta_lookup table.

Coupons: A Non-Issue

Going from 50 to 10,000 coupons with 500K _used_by postmeta entries produced zero measurable performance difference. WooCommerce's coupon validation uses (post_id, meta_key) indexed lookups that are O(1) regardless of total table size.

Multisite: Fine Until 500 VU

I set up two WordPress multisites (subdirectory mode, 8 WooCommerce stores each) on the same server. Same Redis, same OLS, same catalog. Then ran the full k6 suite at 1-500 VU.

Multisite Checkout Overhead vs Single-Site
LoadClassic (single)Classic (multi)OverheadSA (single)SA (multi)Overhead
1 VU185ms190ms+3%225ms232ms+3%
50 VU1,642ms1,855ms+13%2,673ms2,761ms+3%
200 VU2,107ms3,179ms+51%4,706ms4,950ms+5%
500 VU2,643msFAIL100% errors5,986ms30,001ms100% errors
Store API is more multisite-friendly under load. At 200 VU, Classic checkout overhead is +51% on multisite while Store API is only +5%. Classic's session-based DB queries contend with shared tables. Store API's stateless REST approach avoids session table contention. At 500 VU, both collapse (100% checkout failure), but Store API's cart still functions (2.4% failure vs Classic's 16.3%).

The Verdict

Neither approach won outright. Classic is faster where it's simple (placing orders, first-visit page load). Store API is more consistent where it's complex (cart operations under load, returning visitor experience, heavy data stores). The six numbers below capture the trade-off.

5% Classic's total checkout is 5% faster with Stripe (16.4s vs 17.2s). With COD the Place Order gap was 22-127%, but Stripe's ~5s API latency dominates both.
38x Store API's cart P95 at 200 VU: 268ms vs Classic's 10,220ms. 5% of Classic users wait 10+ seconds.
~8% Store API's total checkout interaction (page load + fill + wait) is faster on both cold and warm visits.
2x Store API's update wait after filling fields: 1,004ms vs Classic's 2,007ms. Server-side, unchanged by cache.
9.7x Classic's Real TTI on a mid-range phone (2x CPU): 400ms vs Store API's 3,885ms. React hydration cost on first visit.
123%→40% Classic's checkout lead halves with heavy data (500K orders). Store API benefits more from warm object cache and avoids session table contention.

So Which One Should You Actually Use?

The right answer depends on your store, not these charts.

Go with Store API

Large store with heavy order history

The checkout gap halves with 500K orders: from +123% to +40% at 200 VU. Store API benefits more from warm object cache. Classic's session table and wp_posts queries become bottlenecks at scale.

Heavy cart usage before checkout

Update wait is 2x faster (cache-independent). Total interaction time is 8% better on both cold and warm visits.

Returning visitors on 4G/WiFi

Real TTI under 700ms for both on return visits (even at 4x CPU throttle). Page load: Store API 832ms vs Classic 870ms on no-throttle, tied on 4G. The first-visit JS penalty disappears from cache.

Headless or multi-channel

Proper REST interface. Classic's wc-ajax is tied to the WordPress frontend.

New store, starting fresh

Block checkout is the default in WC 10.x. Classic shortcode is on a deprecation path.

Stick with Classic

Server capacity is the bottleneck

Place Order is 22-127% faster (server-side, unchanged by caching). Fewer PHP workers held per request. Note: full checkout UX favors Store API by ~8%.

Mostly new visitors (ads, social)

Real TTI is 9.7x faster on a mid-range phone (400ms vs 3,885ms). Store API's React hydration creates a 3-second gap where the page is visible but unresponsive. On return visits both are under 700ms.

Significant 3G traffic

3G gap: +37% (first-time), +11% (returning). Payload size still matters on bandwidth-constrained connections.

Third-party checkout plugins

87% of stores still use classic. The plugin ecosystem hasn't caught up with blocks. Verify support before migrating.

Under 50 concurrent users

Performance difference is negligible. Classic gives you a perfect first-visit Lighthouse score and simpler everything.

Extensibility: The Gap That Still Matters

Performance is one axis. The other is how your team customizes the checkout day to day.

Classic is easier to customize today. Add a field: one PHP filter (woocommerce_checkout_fields). Validate it: one action (woocommerce_checkout_process). Save to the order: one hook. 15 years of plugins and Stack Overflow answers back you up. The cost: filter soup. Hooks conflict, ordering matters, debugging cascading filters is miserable.

Block checkout has better architecture and a steeper learning curve. Adding a field requires PHP (ExtendSchema, register_endpoint_data) and JavaScript (Slots & Fills, setExtensionData). Two languages, two steps. But contracts are typed, namespacing prevents conflicts, and the block editor lets non-developers rearrange sections without code. The main compatibility tracking issue is still open, with the WooCommerce team actively working on closing the gap.

The gap is real and documented. Classic hooks like woocommerce_cart_item_visible have no block equivalent yet. Custom fields in block checkout don't apply until the customer selects a country, and complex visibility rules broke in 10.1.0-rc.1 (since fixed). Developers trying custom validation have hit walls that don't exist on classic.

Payment gateways work on both. Stripe, PayPal, Apple Pay, Google Pay, Klarna, Afterpay. Stripe deprecated the old payment form UI in July 2025; the new form works on shortcode pages too.

The decision comes down to three questions: what does your team know, what do your plugins support, and where is your traffic growing? Block checkout was made the default in WC 10.x, so the direction is clear. But the extensibility gap is still open. These benchmarks can inform the timing of your migration. They shouldn't rush it.

What I'd Test Next

A v2 of this test would cover:

  • WooCommerce 10.7+. The WooCommerce team shipped batch endpoint support for all Store API endpoints in 9.8, and they're actively optimizing the checkout pipeline. Expect: the Place Order gap to narrow, cart batch operations to reduce round-trips on mobile.
  • Geographic distribution. All tests hit the server from the same Hetzner datacenter. With real traffic from multiple regions, latency per request increases. Expect: Classic to fare better on high-latency first visits (fewer round-trips), Store API to benefit more from CDN edge caching on return visits.

If you want to run any of these yourself (or reproduce what's already here), the full test suite is open source: github.com/Jorgu5/woocommerce-performance-benchmarks — scripts, Hetzner setup guide, data generators, and every raw result.

Caveats & Findings

One server configuration

Nginx vs LiteSpeed vs Apache, MySQL vs MariaDB, PHP-FPM vs LSPHP will produce different absolute numbers. The relative comparisons should hold.

WC Payments Sift bug

Classic loads checkout.js (including Sift) via woocommerce_after_checkout_form without checking if the gateway is enabled. Block checkout correctly checks is_active() and skips disabled gateways. If you have WC Payments installed but use a different provider, Classic still loads Sift.

🔄

PHP 8.4 + JIT is slower

10-18% slower than 8.3. Requests finish in under 100ms, too fast for JIT's tracing profiler. Stick with 8.3.

🔓

Hidden performance switches

WooCommerce ships features off by default. Without them, Store API tested 9% slower. See the Flags & Perf Switches tab in the terminal at the top.

Add to Cart: PHP Version Comparison

Comments

This article is a living document. I'll re-run the benchmarks with each major WooCommerce release and update the data. If the numbers change, you'll see it here.