The Request Your Cache Never Sees
Your store feels fast. Homepage in 100 milliseconds, category pages snap into place, PageSpeed is green. Then a customer clicks Add to cart — and waits. Two seconds. Three. The little spinner on the button becomes the slowest thing in the shop.
The short answer: WooCommerce add to cart is slow because it is the first request in a customer’s visit that your cache can’t serve. The click is a POST to wc-ajax=add_to_cart (or a full page reload on many product pages) that WooCommerce marks no-cache, no-store — so it boots WordPress, every active plugin and your theme from scratch, validates the product, writes a session and renders the mini-cart. The two seconds you’re seeing aren’t “the cart being slow”. They’re your server and plugin stack, measured for the first time without the cache in front of it.
Table of Contents
That’s the whole disproportion. A cached page is a file: nginx or your CDN hands over HTML that PHP produced hours ago. Add to cart cannot be a file, because the answer depends on who is asking and what’s already in their basket. So it goes the long way — through PHP-FPM, through WordPress, through all of your plugins — and it comes back at whatever speed that stack actually runs.
We’ve written about the same mechanism at the end of the funnel: checkout is slow because checkout can’t be cached. Add to cart is the same truth, earlier and louder. Only a fraction of visitors reach checkout. Every single one who considers buying clicks this button.
What Happens When the Button Is Clicked
There are two ways a WooCommerce store adds a product to the cart, and they cost very different amounts. Before you measure anything, know which one your store is doing on which page.
Path A: the AJAX request (archives by default, product pages if your theme opts in)
This is the path behind the Add to cart buttons on shop and category pages when “Enable AJAX add to cart buttons on archives” is on (it is by default). Click by click:
- The browser —
wc-add-to-cart.jsmarks the buttonloadingand sendsPOST /?wc-ajax=add_to_cartwithproduct_id,quantityand, for variations,variation_id. On block-based themes the equivalent isPOST /wp-json/wc/store/v1/cart/add-item. - nginx → PHP-FPM. No page cache rule matches a POST, so the request goes straight to PHP. A worker is taken from the pool; if the pool is busy, the request queues here first.
- WordPress boots. Core, then every active plugin, then your theme’s
functions.php, plus every autoloaded option fromwp_options. Nothing about this step is specific to the cart — it is the exact cost of your stack, and it’s paid before a single line of WooCommerce cart code runs. - WooCommerce takes over on
template_redirect.WC_AJAXrecognises thewc-ajaxquery var, definesDOING_AJAX, and sends the headers that matter for this article:Cache-Control: no-cache, must-revalidate, max-age=0, no-store, privateand anExpiresdate in 1984. - Validation. The
woocommerce_add_to_cart_validationfilter runs — stock, purchasability, “sold individually”, and whatever your plugins hang on it: minimum/maximum quantity rules, role-based pricing, bundles, subscriptions. - The product is loaded —
wc_get_product(), which means post, postmeta and lookup-table reads. With a persistent object cache these are memory hits; without one, they’re database queries. - The cart item is added, and
woocommerce_add_to_cartfires. This is the hook analytics, pixel and CRM plugins love. If one of them makes a synchronous HTTP call here — a server-side conversion event, an ERP stock check, an anti-fraud lookup — the customer waits for that remote server too. - The session is written. WooCommerce stores the cart in
wp_woocommerce_sessionsand sets three cookies:wp_woocommerce_session_*,woocommerce_items_in_cartandwoocommerce_cart_hash. - The mini-cart is rendered.
woocommerce_mini_cart()runs your theme’s mini-cart template;woocommerce_add_to_cart_fragmentslets themes and side-cart plugins bolt on more HTML. All of it is serialised to JSON and sent back. - The browser swaps the fragments into the header and fires
added_to_cart. Only now does the spinner stop.
Steps 3 and 9 are where the milliseconds hide. Steps 5–8 are cheap on a healthy store — a few milliseconds — unless a plugin makes them expensive.
Path B: the full page reload (the default on single product pages)
Here’s the part most guides miss. On the single product page, stock WooCommerce does not use AJAX. The button is a plain form submit:
<form class="cart" action="https://store.com/product/globe-80/" method="post">
…
<button type="submit" name="add-to-cart" value="7445" class="single_add_to_cart_button button alt">
</form>
The browser POSTs to the product URL. WooCommerce’s form handler adds the item on wp_loaded — same validation, same session write as above — and then the entire product page renders again, uncached, with the “has been added to your cart” notice on top. There’s no redirect unless “Redirect to the cart page after successful addition” is on, in which case the customer gets a second uncached page (the cart) on top of the first.
So on the page where most add-to-cart clicks happen, the customer isn’t waiting for a 3 KB JSON response. They’re waiting for a full product page render — gallery, related products, upsells, reviews, every widget — at bare server speed. Themes can switch this to AJAX (Flatsome has a toggle, Astra and others too), but many stores never turn it on.
Where the time goes:
| Step | Path A (AJAX) | Path B (reload) | Layer it belongs to |
|---|---|---|---|
| PHP-FPM queueing | if the pool is saturated | same | Server |
| WordPress + plugins + theme boot | the bulk | the bulk | Server / stack |
| Validation, product load, cart add, session | milliseconds | milliseconds | Database (or a plugin) |
Hooks on woocommerce_add_to_cart | 0 ms — or a remote API round-trip | same | External APIs |
| Rendering | mini-cart template | the whole product page | Theme / frontend |
Why It Can’t Be Cached (and Why That’s Useful)
Four reasons, stacked, and any one of them alone would be enough:
- It’s a POST. Page caches — nginx FastCGI cache, WP Rocket, LiteSpeed, Cloudflare — store responses to GET requests. A POST bypasses every one of them by design.
- WooCommerce tells caches not to store it. Every
wc-ajaxresponse carriesCache-Control: no-cache, no-store, privateand anExpiresheader from 1984. A CDN that honoured a cached copy would be broken. - The answer is personal. The response contains this customer’s cart. Serving it to anyone else would be wrong, not just stale.
- The cookie poisons the well. Once
woocommerce_items_in_cart=1is set, most cache layers bypass the page cache for that visitor on every page, so that the header cart count and the mini-cart stay correct. From the first click onward, that customer is browsing your store the way your server really is.
Here’s the useful part. Because nothing sits between the click and PHP, the TTFB of wc-ajax=add_to_cart is a clean measurement of your WordPress stack — the same thing the “empty page” step of the checkout diagnosis measures with Query Monitor, but taken from the outside, in a request every customer makes. Your cache hides that number on every other page. Add to cart shows it.
A caching plugin cannot fix a slow add to cart. It can only make the pages around it fast enough to make the contrast hurt.
What the Numbers Look Like on a Fast Server
Measured on 4 September 2026 on a staging copy of loftlight.pl, a WooCommerce store we look after. Setup: dedicated Hetzner server, Intel Core i5-13500 (up to 4.8 GHz), GridPane, nginx + PHP-FPM 8.3, OPcache on, no CDN and no page cache in the path. WordPress 7.1, WooCommerce 11.0.1, 32 active plugins, Flatsome child theme, HPOS. Flatsome’s AJAX-on-product-page option is off, so the product page uses Path B. All requests sent with curl from the server to itself, so the numbers are pure server time (web server + PHP), no network. Ten samples for the AJAX requests, five for the pages — medians shown.
| Request | Server time (median) | What it measures |
|---|---|---|
hello.php — a PHP file that echoes a timestamp | 1 ms | Infrastructure floor (same server, from the checkout article) |
wc-ajax=get_refreshed_fragments, empty cart | 113 ms | WordPress + 32 plugins + theme boot, mini-cart render, no cart logic |
wc-ajax=add_to_cart, new session | 127 ms | The click, Path A |
wc-ajax=add_to_cart, existing session | 127 ms | Same, second product |
Store API GET /wc/store/v1/cart, empty | 123 ms | The block-theme flavour of the same boot |
| Product page, uncached, no cart | 324 ms | What Path B has to render |
/product/…/?add-to-cart=7445 | 297 ms | The click, Path B: add + full page in one request |
Reading it bottom-up:
- Infrastructure: 1 ms. The floor is effectively zero. Everything above it is code.
- The stack: 113 ms. A request that adds nothing to the cart — it just boots WordPress and renders an empty mini-cart — costs 113 ms. That’s 32 plugins and a theme initialising, on a 4.8 GHz core with OPcache warm. This is the number the cache hides on every other page.
- The cart logic: 14 ms. Validation, product load, cart add, session write and the mini-cart template add 14 ms on top. 89% of the add-to-cart request is the stack, not the cart. Session creation is free — the second product costs the same as the first.
- Path B: 2.3× Path A. On the product page, the same click costs 297 ms, because it renders the whole page. Nothing about the cart got slower; the request just does more rendering.
Now the physics. On a 2.0–2.5 GHz shared core the same PHP runs roughly twice as slow before noisy neighbours, a cold OPcache, or a saturated FPM pool. Double the plugin count and you double the boot. Add one plugin that calls a remote API on the add-to-cart hook and you add that API’s latency, plus its timeout on a bad day. That’s how 127 ms becomes 2 seconds without any single thing being “broken”.
One small find from this run, because it’s the shape of the problem in miniature: Query Monitor flagged a PHP warning on every add-to-cart request from Disable Admin Notices — an admin-only plugin loading and unserialising its options on a frontend AJAX call, and failing on a corrupted blob each time. Milliseconds here. But it’s code with no business in the request, running on every click, and stores accumulate a dozen of these.
How to Diagnose a Slow Add to Cart
Don’t guess. Measure, and measure the click itself — not the page it’s on. The same three rules as for checkout: measure server time, not page load; take five samples and use the median; do it on a staging copy that’s functionally identical but externally dead if you can.
Step 1: Network tab — five clicks
Open DevTools → Network, go to a category page and click Add to cart five times (different products, or the same one — it doesn’t matter). Filter by wc-ajax (classic themes) or wc/store (block themes). For each request, note Waiting (TTFB).
Then do the same on a single product page — and look at what actually fires. If you see a wc-ajax=add_to_cart request, your theme uses AJAX there too. If instead you see a document request to the product URL itself (method POST, status 200), you’re on Path B: the number to write down is that page’s TTFB, and you should know that it includes a full render.
| Add-to-cart TTFB (from the browser) | Read as |
|---|---|
| under 300 ms | Healthy stack. If the click still feels slow, it’s the frontend: JS waiting on something else, a slow fragments swap, a side-cart animation. |
| 300 ms – 1 s | Normal on shared hosting with a typical plugin count. Worth fixing; the same stack cost is inside every checkout request too. |
| over 1 s | Something specific is expensive — Step 2 and 3 say what. |
Step 2: Log in and let Query Monitor read the request
Install Query Monitor, log in as an admin, and click the button again. Query Monitor can’t draw its panel on a JSON response, but for AJAX and REST requests it sends its overview in HTTP headers and prints it in the browser console — you’ll see a “Query Monitor” group with the time taken and memory used. If your theme’s request doesn’t show in the console, open the request in the Network tab and read the X-QM-overview-time-taken response header directly. That’s the PHP generation time of the click.
Now compare it with the empty page generation time from the checkout diagnosis — a blank page, logged in, Query Monitor’s Overview panel. That number is your stack. The add-to-cart request should cost roughly the same plus a few milliseconds:
| Add-to-cart time vs empty page | Diagnosis |
|---|---|
| About the same (within ~50 ms) | The click is just your stack. Fix the stack: server, OPcache, plugin count, autoload. |
| 1.5–3× the empty page | Something hooks into the cart. Step 3 finds it. |
| Seconds, with the empty page fast | A remote call on the add-to-cart path — Step 3, HTTP API Calls panel. |
(Logged-in admin requests carry ~40 ms of extra overhead — the admin bar, Query Monitor itself. It’s constant, so it cancels out in the comparison.)
Step 3: See the queries and the hooks — the ?add-to-cart=ID trick
To see inside the cart logic you need the full Query Monitor panel, and for that you need a page, not a JSON response. WooCommerce’s form handler accepts the product in the URL, so load your product page with ?add-to-cart=ID appended, logged in:
https://store.com/product/globe-80/?add-to-cart=7445
That one request adds the product and renders the page with Query Monitor attached. Now open the panels and compare with a plain load of the same product page:
- HTTP API Calls — anything new here is a plugin phoning out on the add-to-cart hooks: pixels and server-side conversion APIs, ERP or PIM stock checks, anti-fraud, address validation. Each one is a synchronous round-trip the customer waits for, and each has a timeout that becomes the worst case.
- Queries by Component — a component whose query count jumps between the two loads is doing work on the click: dynamic pricing recalculating the cart, bundle plugins loading child products, “recently viewed” and wishlist plugins writing rows.
- Database time — if it jumps and the new queries are on
postmeta,wc_product_meta_lookupor the sessions table, you’re paying for product data without an object cache, or for a bloated sessions table.
Remove the product from the cart between samples so each load adds it fresh. And if PHP time grows with no new queries and no HTTP calls, only a profiler tells you which plugin — that’s Code Profiler or wp profile territory.
How to Fix a Slow Add to Cart
By what the diagnosis found — and none of it is “install a caching plugin”.
If the click costs about the same as an empty page: fix the stack
The cart is innocent; your WordPress floor is high. This is the checkout article’s Layer 1 and 2, and the fixes are the same:
- CPU clock speed and OPcache first. A 4.8 GHz core boots the loftlight stack in 113 ms. Halve the clock and you double it before anything else goes wrong. Check
lscpu, check OPcache is on and large enough — it’s the single cheapest win we see. - Fewer plugins in the request. Every active plugin pays its initialisation cost on every add to cart, including the ones that only do admin work. Deactivate what you don’t use; for what you do, check whether it has any reason to load on the frontend.
- Autoloaded options and a persistent object cache. The boot reads every autoloaded option; the cart logic reads product data. Trim the first, cache the second with Redis.
If plugins hook into the click: move the work off the request
- Server-side pixels and conversion APIs (Meta CAPI, GA4 Measurement Protocol, TikTok events) fired synchronously on
woocommerce_add_to_cartare the most common find. Configure them to queue — most decent implementations use Action Scheduler or a cron — or accept the round-trip knowingly. - Stock checks against an ERP belong in a sync job, not in the click. The customer doesn’t need a live warehouse answer to put an item in the basket; they need it at checkout.
- Pricing, bundle and quantity-rule plugins are legitimate work on the cart — but measure them one at a time on staging. One of them is usually doing far more than the others.
If you’re on Path B: make the reload cheap, or stop reloading
- Turn on AJAX add to cart on the product page if your theme supports it. That alone turns a full page render into a 3 KB JSON response — on loftlight, 297 ms becomes 127 ms with no other change.
- Keep “Redirect to the cart page after successful addition” off unless your funnel genuinely needs the interruption. On, it costs a second uncached page per click.
- If you must reload, make the product page lean: related-product carousels, upsell queries and review widgets all run again, uncached, on every add.
If the JSON is huge: slim the fragments
Look at the response size in the Network tab. Stock WooCommerce returns about 3 KB — a mini-cart. Some themes and side-cart plugins return the entire cart drawer, cross-sells included, at 50–100 KB per click. That’s rendering time on the server and parsing time on the phone. Fewer fragments, simpler mini-cart template.
What doesn’t help
A page cache, a CDN, “optimisation” plugins that minify JavaScript, preloading — all of it works on the pages around the click. The click itself runs at bare server speed no matter what sits in front of nginx. The only caches that touch it are OPcache (bytecode) and an object cache (data).
The Same Truth, Earlier in the Funnel
Add to cart is where your customers first meet your server. Everything before it was a cached file; everything after it — cart, checkout, payment — runs the way this request runs. If the click takes two seconds, checkout takes at least that, on every field change.
That’s also why it’s the best early warning you have. You don’t need a load test or a profiler to notice it. You need to click the button on your own store, on your phone, on mobile data, and count.
Every store we take on through WooCommerce Care gets this measured first — the click, the empty page, the checkout request — so that we know which layer we’re fixing before we touch anything. Then automated Playwright tests keep clicking that button after every update, so it stays fast.
If your add to cart takes more than a second and you don’t know where the time goes — that’s exactly what we diagnose.
FAQ: Slow WooCommerce Add to Cart
Why is WooCommerce add to cart slow when the rest of the site is fast?
Because the rest of the site is served from cache and add to cart can’t be. The click is a POST that WooCommerce marks no-cache, no-store, so it boots WordPress, every plugin and your theme from scratch, validates the product, writes a session and renders the mini-cart. Its response time is your real server speed; the fast pages were the cache’s speed.
Why is the WooCommerce add to cart button slow or stuck loading?
The button shows a spinner until the wc-ajax=add_to_cart request returns. If it returns slowly, see above — measure its TTFB in the Network tab. If it never returns to normal, the request probably failed: a JavaScript error in the console, a 403 from a security plugin or firewall blocking the POST, or a plugin returning invalid JSON on the add-to-cart hook. Check the request’s status code and response body first.
Does a caching plugin speed up WooCommerce add to cart?
No. Page caches store GET responses for anonymous visitors; add to cart is a POST with per-customer content and explicit no-cache headers. What does help is OPcache (PHP bytecode), a persistent object cache like Redis (product and option data), and fewer plugins booting on every request.
Is the WooCommerce cart page slow for the same reason?
Yes. The cart page, like checkout and My Account, holds customer-specific data and is excluded from page cache. It runs the same boot plus the cart totals calculation and the cart template. On the store measured above, the cart page with one product took 305 ms under load — same order of magnitude as the click.
Do cart fragments make WooCommerce slow?
wc-ajax=get_refreshed_fragments is the request that keeps the header cart count and mini-cart current, and it costs the full WordPress boot just like add to cart (113 ms on the store above, on a fast server). WooCommerce throttles it — fragments are cached in the browser’s sessionStorage and only refreshed when the cart hash changes — but some themes and mini-cart plugins fire it on every page load anyway. If you see it on every navigation with an empty cart, that’s worth fixing. Don’t disable the script blindly: the mini-cart stops updating.
Add to Cart Diagnostic Checklist
Before optimising, measure — in this order:
- Which path each page uses: AJAX (
wc-ajax/wc/storerequest) or a full reload (document POST to the product URL) wc-ajax=add_to_cartTTFB, Network tab, 5 clicks, median- The same click logged in with Query Monitor:
X-QM-overview-time-taken - Empty page generation time (Query Monitor, Overview) — your stack floor
/product/…/?add-to-cart=IDlogged in: HTTP API Calls panel, Queries by Component — delta over a plain product page- Response size of the add-to-cart JSON
- CPU model and clock speed (
lscpu), OPcache status - Active plugin count — and how many of them have no frontend job
- Theme setting for AJAX on the single product page; WooCommerce “redirect to cart after add” setting