Try 14 days free
Skip to content

WooCommerce Store API collection-data: why it got capped at 25

The Store API endpoint products/collection-data is public and unauthenticated, and until a 2026 fix, the calculate_attribute_counts and calculate_taxonomy_counts parameters it accepts had no size limit. One request with 5,000 entries cost about 30,000 database queries and 10.5 seconds on a 10,000-product store — a single plain request, no login required. InstantFilter counts facets in the browser after hydration and has no public endpoint that runs an aggregate query per requested count at all.

What does the collection-data endpoint actually do?

wc/store/v1/products/collection-data backs the native WooCommerce Blocks filters — the attribute, taxonomy, price, rating, and stock filter blocks you place with the block editor. A client asks it for counts: “how many products match this category if I also filter by this attribute.” The request can include a calculate_attribute_counts array and a calculate_taxonomy_counts array, and the endpoint runs one full-collection aggregate query per entry in those arrays, then returns all the counts in one response.

That design is reasonable for the UI it was built for: a sidebar with a handful of filter blocks asks for a handful of counts. The PR that fixed this, merged into WooCommerce core in June 2026, describes the actual first-party caller precisely: “the legacy attribute filter, which dedupes per attribute… a real sidebar asks for ~3–10 attributes.” The endpoint itself did not restrict the request to that shape.

Why could one request trigger thousands of database queries?

Because nothing capped how many entries those two arrays could contain, and the cost scaled linearly with the entry count. The WooCommerce engineer who authored the fix published exact numbers from a local 10,000-product, no-object-cache environment, in the pull request discussion:

25 entries: roughly 150 queries, about 0.14 seconds. 1,000 entries: roughly 6,000 queries, about 2.2 seconds. 5,000 entries: roughly 30,000 queries, about 10.5 seconds — all from a single unauthenticated request. The PR notes a plain GET is limited by URL length, but the same payload is reachable unbounded through POST …?_method=GET with the entries carried in the request body, and that path was confirmed to return HTTP 200 after 10.5 seconds for 5,000 entries. “A few concurrent requests saturate the DB,” the author wrote — this is not a one-off slow page, it is a lever that scales with however many requests run at once.

A separate live-staging test in the same PR, on a roughly 1,000-product store behind a CDN and WAF, showed the real-world effect: under a short load test of the unpatched endpoint, backend response time rose from well under a second to several seconds, CPU rose from about 1–4% idle to 100%, memory rose by over 2,800 MB, and a normal storefront request not served from the CDN cache timed out. The site recovered within seconds once the test stopped — the damage was load-bound, not a crash, but it was real on real hosting.

How do you check whether your store has the fix?

WooCommerce’s fix hardcodes a cap of 25 entries per request (the COUNTS_MAX_ITEMS constant backing the REST schema’s maxItems), with no filter to raise it — the maintainers deliberately rejected making the limit configurable, reasoning that “the attack only works against stores on the defaults, so an opt-in limit would protect only the stores that already changed it.” On a store running the fixed version, a request asking for more than 25 counts gets an immediate HTTP 400 with the error code rest_invalid_param, returned before any query runs — not a slow response, a fast rejection.

The practical check: update to a current WooCommerce version and confirm the changelog includes the entry limiting and de-duplicating products/collection-data count requests. If you manage a store on an older, unpatched version and cannot update immediately, watching the access log for repeated POST requests to wc/store/v1/products/collection-data with an unusually large body, or for the query string trick ?_method=GET on that route, is a reasonable interim signal — the same signal that shows up in a load-test graph as a sudden CPU and memory spike with no corresponding rise in normal storefront traffic.

Why wasn’t de-duplication or caching enough by itself?

Because both existed already and neither bounded the worst case. array_unique only collapses exact string duplicates — distinct-looking variants, like differently-cased taxonomy names, passed straight through until the fix moved sanitization ahead of the dedup step. The transient cache behind the counts is bypassed by varying the search parameter on each request, making every request a cold cache miss, and requesting counts for taxonomies that don’t exist or aren’t attributes meant those invalid entries re-ran their lookup every time instead of being rejected in memory. The fix adds all three: sanitize-then-dedupe, a hard numeric cap, and an in-memory taxonomy_exists() / taxonomy_is_product_attribute() check before any database query runs. The cap is what actually bounds the cost; the other two reduce wasted work on top of that bound.

Why did maintainers reject making the limit configurable?

An early review round suggested an opt-in limit, so stores could choose whether to adopt it. The author’s response, in the PR thread, lays out the reasoning plainly: “the attack only works against stores on the defaults, so an opt-in limit would protect only the stores that already changed it, i.e. not the ones at risk.” The fix needed to be default-on to protect the stores that needed it, which are by definition the ones that never touch this setting. The author also pointed to precedent already inside WordPress core and WooCommerce’s own REST API for this exact pattern — default-on, reject-style bounds rather than opt-in configuration: WordPress core’s own batch endpoints cap at 25 items, per_page is capped at 100 across WP REST routes, and woocommerce_rest_batch_items_limit already rejects oversized WooCommerce REST batches with an HTTP 413. A 25-item cap on collection-data counts is the same category of guardrail, not a new precedent.

That exchange matters for anyone extending the Store API with a custom block or a headless frontend that calls collection-data directly: the cap is hardcoded at 25, with no filter to raise it, by deliberate choice. A build asking for more than 25 attribute or taxonomy counts in a single request needs to split that into multiple requests — there is no supported way to opt back into the old unbounded behavior, and the maintainers’ own words make clear that absence is intentional.

Is this the same issue as the slow collection-data subqueries?

Related, and fixed by the same pull request, but originally a separate complaint. In early 2025 a developer using get_attribute_counts() in custom code reported, in a now-closed GitHub issue, that the method’s subqueries prevented MySQL from using indices efficiently: “the methods in the filter class make a lot use of subqueries which will then be passed into the main query of the method… MYSQL cannot use indices when using subqueries which is a potential performance impact… reported as a slow query.” That issue tracked a query-plan problem — each individual count query running slower than it needed to — independent of how many counts a request asked for.

The two problems ended up sharing a fix. The same June 2026 pull request that added the 25-entry cap also rewrote get_attribute_counts() to go through the shared FilterDataProvider service — the maintainer closing the older issue noted plainly: “Closing this since the attribute count is now powered by FilterData service in” that PR. The cap bounds how many aggregate queries one request can trigger; the FilterDataProvider rewrite is what made each of those queries cheaper and cacheable in the first place. A store that only updates far enough to get one half of that PR, if that were even possible, would still carry the other problem — in practice both landed together, which is why checking for the current WooCommerce version covers both at once rather than requiring two separate fixes.

Does InstantFilter have an equivalent endpoint to worry about?

No. InstantFilter does not expose a public endpoint that accepts a list of attributes and runs one aggregate query per entry — facet counts are computed in the browser, against the codebook already downloaded for the current archive, after the first render. There is no request shape to cap because there is no per-count request at all once hydration has happened; the server-side cost of counting is paid once, during the indexing sync you trigger yourself, not once per visitor request. That does not make the Store API’s fix less worth applying if you also use WooCommerce’s native Blocks filters anywhere on the site — it means the architecture behind filtering on InstantFilter’s rendered archives doesn’t share this particular attack surface.

Keep going

The Store API counts endpoint is one request shape among several covered in this cluster.

Questions about the Store API collection-data endpoint

It is a Store API route inside WooCommerce core (plugins/woocommerce), used by the native product filter blocks. It affects any store running an unpatched WooCommerce version, independent of which theme or page builder you use.
No. A request within the 25-entry cap returns the same counts as before, for all terms in every requested taxonomy. The cap limits how many taxonomies one request can ask about, not how many terms come back per taxonomy.
The PR author measured the first-party caller at roughly 3 to 10 attributes per request, taking about 0.1 seconds — well under the 25-entry cap that was added.
No. InstantFilter does not use WooCommerce’s Blocks filter counting endpoint. Facet counts come from the client-side codebook built during InstantFilter’s own sync step.

Ready to make filtering instant?

Download the free 14-day trial of Pro. No credit card required — test immediately.