Headless shops are fast in 2026 - as long as the Store API keeps up. This is exactly where Shopware steps in from version 6.7: selected, non-mutating Store API endpoints are now cacheable and return real Cache-Control headers. This shifts a large share of the load away from the origin onto reverse proxy, CDN and the frontend. The lever is substantial: a mere 0.1 second faster response increased retail conversions by 8.4% and average order value by 9.2% (Deloitte) - and good Core Web Vitals lift conversion by 15 to 30% (web.dev). This guide shows how to avoid N+1 problems in Nuxt and composable frontends, set client caching correctly and use stale-while-revalidate in production - purely on the Store API caching layer, distinct from the architecture question.

Why Store API caching is the 2026 performance lever

In a headless setup the frontend - for example Nuxt or a composable UI - renders the interface and fetches data through the Store API. Unlike the classic storefront, which brings its own HTTP cache and ESI, Store API responses long ran uncached straight from the origin. Every page view, every category, every product block triggered a fresh PHP request including a database hit. As traffic grows, this becomes the bottleneck.

The numbers make the pressure clear: 53% of mobile users leave a page that takes longer than 3 seconds to load (Google/SOASTA), and conversion drops by around 7% for every additional second of load time (Conductor). Just 100 ms of extra time can cost around 1% of revenue (Amazon via Conductor). Making the Store API cacheable addresses exactly this latency - not in the frontend code, but at the most expensive point: server response time.

Distinction from architecture topics

This article deals exclusively with the Store API caching layer. If you first want to make the fundamental decision for a decoupled frontend, read Headless Commerce with Shopware and Composable Commerce. Here the focus is on the concrete caching mechanics, not on the architecture question.

What changes concretely from Shopware 6.7

With the reworked caching system (introduced via the CACHE_REWORK feature flag and rolled out in production since the 6.7.5/6.7.6 releases), Shopware marks selected Store API routes as cacheable. A route is cacheable when it carries the route attribute '_httpCache' => true - meaning non-mutating endpoints that return no sensitive, customer-specific data (Shopware Docs).

Cacheable routes return the following Cache-Control header by default (Shopware Docs):

Cache-Control (cacheable route)
Cache-Control: public, max-age=0, s-maxage=1800,
               stale-while-revalidate=86400, stale-if-error=7200

# Non-cacheable route:
Cache-Control: no-cache, private

s-maxage=1800

Shared caches (reverse proxy, CDN) keep the response fresh for 30 minutes. max-age=0 forces the browser to revalidate - so you control server and client cache separately.

stale-while-revalidate=86400

For up to 24 hours the cache may serve a stale response instantly and refresh it in the background. The user does not wait for the origin.

stale-if-error=7200

If the origin fails, the cache keeps serving the last good response for 2 hours - a built-in safety net for peak load.

public

The response may be stored by shared caches - the prerequisite for a reverse proxy or CDN to step in at all.

So that separate languages, currencies and contexts do not share the same cache entry, Shopware uses three dedicated headers instead of cookies, carried in the Vary header: sw-currency-id, sw-language-id and sw-context-hash (Shopware Docs). This way a DE/EUR visitor does not accidentally receive the EN/USD response from the cache.

GET instead of POST: the prerequisite for caching

HTTP caches usually store only GET responses. Many Store API calls previously used POST to pass complex filter and search criteria in the body. To make these requests cacheable, criteria can now be passed as a compressed, encoded string in the GET parameter _criteria - internally encoded as JSON -> gzip -> base64url (Shopware Docs).

store-api-client.ts
// Cacheable GET request with encoded criteria
const criteria = {
  limit: 24,
  associations: { cover: {}, manufacturer: {} },
  filter: [{ type: 'equals', field: 'active', value: true }]
}

const encoded = encodeCriteria(criteria) // JSON -> gzip -> base64url
const res = await $fetch(
  `/store-api/product?_criteria=${encoded}`,
  { headers: { 'sw-access-key': key } }
)
// the response now carries a public Cache-Control header
Canonicalize criteria

The SDK helpers canonicalize the criteria before encoding - same request, same string, same cache key. Anyone building criteria themselves should keep order and fields stable, otherwise the hit rate drops because semantically identical requests produce different cache entries.

Avoiding N+1 problems in Nuxt and composable frontends

The most common performance leak in headless frontends is not the missing cache but the N+1 pattern: a list component renders 40 products and fires its own detail call per product. One request becomes 41 - and even with caching that stays inefficient, because every call brings its own latency, connection and revalidation.

PatternCalls per list pageLatency riskCache efficiency
N+1 (one call per product)41HighLow
Batched (associations)1-2LowHigh
Batched + SWR1-2 (often from cache)Very lowVery high

The solution lies in the Store API itself: instead of reloading per product, you request related data via associations and includes in a single call. This reduces not only the number of requests but also the transferred data volume - includes trims the response to the fields you actually need.

useProductListing.ts
// Instead of 40 single calls: one batched, lean request
const { data } = await useAsyncData('listing', () =>
  storeApi('/store-api/product-listing/' + categoryId, {
    method: 'GET',
    query: { _criteria: encodeCriteria({
      associations: { cover: {}, options: {} },
      includes: {
        product: ['id', 'name', 'calculatedPrice', 'cover'],
        product_media: ['url']
      }
    }) }
  })
)
// useAsyncData deduplicates parallel calls automatically
  • Batch instead of reload: Fetch related entities via associations in one call, not per product.
  • Keep the payload lean: Use includes to request only the fields the UI actually renders - fewer bytes, faster parsing.
  • Request deduplication:useAsyncData/useFetch in Nuxt bundle identical parallel calls into one request with a shared key.
  • Pagination before prefetch: Load the visible page first, prefetch following pages only on demand or when idle.

Setting client caching correctly in the frontend

Server caching addresses response time, client caching addresses navigation. A composable frontend can keep already loaded Store API responses in memory and render them instantly from the normalized cache on repeat access - while revalidating in the background. Page changes feel instant. Studies quantify the effect: React Server Components and lean hydration reduce the JavaScript bundle by 40 to 60% (digitalapplied), directly improving INP and LCP.

The key is alignment with the server headers. Because max-age=0 is set, the browser revalidates - but a conditional request with ETag or If-None-Match ends in a lean 304 Not Modified instead of a full response when data is unchanged. That saves bandwidth without sacrificing freshness.

Memory cache

Keep already loaded lists and products in the state store. Returning navigation renders without a new network roundtrip.

ETag / 304

Use conditional requests: unchanged data returns 304 Not Modified - the body is not transferred at all.

Prefetch when idle

Preload likely next routes (e.g. top category) during idle time so the click feels instant.

Never cache personalized data

Cart, customer account and prices with customer-specific discounts do NOT belong in a public cache. These endpoints stay no-cache, private. Strictly separate cacheable catalog data from context-dependent data - otherwise you risk a user seeing foreign or stale personalized content.

Using stale-while-revalidate in production

Stale-while-revalidate (SWR) is the most effective lever for perceived speed. The principle: once s-maxage expires, the cache still serves the stale response instantly and triggers a refresh in the background. Nobody waits for the origin - the next visitor already gets the fresh version. In practice, SWR headers cut origin hits by around 65% (digitalapplied).

Stale-while-revalidate decouples perceived speed from actual origin response time: the user sees content immediately, while the refresh happens invisibly in the background.

XICTRON development team

The Shopware defaults are deliberately generous: stale-while-revalidate=86400 (24 hours) and stale-if-error=7200 (2 hours) ensure that content is still served even with rare traffic or a brief origin outage. For fast-moving content - such as availability or promotional prices - tighter values can be configured per route via named caching policies that form Cache-Control per area (storefront, store_api) and route.

BFF response for aggregated data
# A dedicated backend-for-frontend layer that bundles
# multiple Store API sources can set tighter values:
Cache-Control: public, s-maxage=60, stale-while-revalidate=600
The SWR rule of thumb

Set s-maxage as short as needed for freshness and stale-while-revalidate as long as possible for availability. This keeps the hit rate high without users seeing stale content over longer periods - the background refresh keeps the cache current.

Cache invalidation: fresh, without a bottleneck

A cache is only as good as its invalidation. When a price or stock level changes, the affected entry must expire selectively - not the entire cache. Shopware works here with cache tags that synchronize the object cache, the HTTP cache and - configured cleanly - the edge layer too. This keeps the hit rate high without stale data hanging around.

  1. Invalidate by tag: On a product change, clear only the related cache tags, do not flush globally.
  2. SWR as a buffer: Even right after expiry the cache serves instantly and fetches fresh data in the background - no load spike on the origin.
  3. Monitor the hit rate: A falling hit rate is often the first signal of fragmented cache keys or overly tight TTLs - monitor performance regularly.
  4. Check context headers: Incorrectly set Vary headers fragment the cache and reduce the hit rate drastically.
Interplay with edge caching

Store API caching and edge caching for Shopware complement each other: the Store API delivers the Cache-Control headers, the edge node enforces them globally. Combining both brings API responses worldwide under the 50 ms mark.

Measure what matters: headless performance audit

Caching only works when it is measured. A headless performance audit checks which Store API routes are actually cacheable, how high the real hit rate is, whether N+1 patterns lurk in the frontend and whether the Vary headers are set cleanly. The effect is measurable: edge and API caching typically reduce origin requests by 85 to 95% (digitalapplied) and TTFB by 60 to 80% (Cloudflare).

At the same time: a poorly planned headless rebuild can cost 20 to 40% of organic traffic (digitalapplied) when rendering, caching and SEO do not align. That is exactly why the caching layer and Core Web Vitals optimization belong in the same analysis. XICTRON examines both levels together and derives concrete, prioritized measures.

  • Cacheable Store API routes identified and _httpCache set correctly
  • GET with _criteria instead of POST for cacheable queries
  • N+1 patterns in the Nuxt/composable frontend replaced by batching
  • Vary headers (sw-currency-id, sw-language-id, sw-context-hash) correct
  • Personalized endpoints strictly separated from public cache
  • Tag-based invalidation and hit-rate monitoring established

Backend-for-frontend: bundling multiple sources cleanly

Many headless pages combine several Store API responses into one view: a product list, plus the navigation menu, cross-selling and a CMS block. If these calls are fired directly from the browser, latencies and connections add up. A backend-for-frontend (BFF) bundles them server-side into a single, prepared response - and can set its own tighter Cache-Control values, because it knows the freshness of the mixture itself.

The effect is twofold: first, the client avoids the waterfall of individual requests; second, the aggregated response itself becomes cacheable. A BFF with public, s-maxage=60, stale-while-revalidate=600 serves the composition fresh for 60 seconds and then as a stale response for up to ten minutes, while it is recomposed in the background. It is important that the BFF passes through the context headers (sw-language-id, sw-currency-id, sw-context-hash) and carries them in Vary, so the aggregation is not confused across language and currency boundaries.

Do not overload the BFF

A BFF should compose, not personalize. As soon as customer-specific data such as cart or individual prices flow in, the response drops out of the public cache. Separate the cacheable catalog part from the personalized part and load the latter separately in the client - this keeps the large, expensive part of the page cacheable.

In practice the BFF pays off especially for entry and category pages with a large static catalog share. Product detail pages benefit too, as long as availability and price do not need to be accurate to the second. Where they do, you combine a cacheable base state with a small, uncached live request for the volatile part.

Common caching mistakes and how to avoid them

Caching only works when it is thought through consistently. In headless projects we repeatedly encounter the same patterns that depress the hit rate or - worse - serve incorrect data. The good news: they can be avoided with clear rules.

Unstructured cache keys

When criteria are encoded in varying order or with superfluous fields, many slightly different keys arise for the same request. Solution: canonicalize criteria before encoding and request only fields you actually use.

Personalization in the public cache

Anyone accidentally mixing customer-specific prices or recommendations into a cacheable route risks a visitor seeing foreign data. Solution: strictly move personalized parts into private routes or client calls.

Overly aggressive invalidation

A blanket cache flush on every change drops the hit rate to zero and creates load spikes. Solution: invalidate only affected entries by tag and use stale-while-revalidate as a buffer.

Missing Vary headers

Without correct Vary entries, languages and currencies share the same entry - or the cache fragments unnecessarily. Solution: carry exactly the three context headers that the response is based on.

There is also an organizational point: caching is not a one-time setup but an ongoing process. Assortments, prices and promotions change, the frontend evolves, new routes are added. That is why the hit rate belongs in monitoring and the caching strategy in every larger release plan. This way the performance gain is sustained instead of fading again after a few weeks.

Roll out step by step: activate caching without risk

Store API caching can be activated safely and step by step, because the reworked caching system sits behind a feature flag and the defaults are conservatively chosen. In practice a staged rollout is advisable: measure on a staging environment first, then start with well-cacheable catalog routes and observe the hit rate before further routes follow. This way you see the effect before it takes hold in production, and you can adjust the TTL values to your assortment.

The order is especially important: first eliminate the N+1 patterns in the frontend, then activate server caching. Anyone who caches first without untangling the frontend merely caches many small, inefficient responses - the gain stays below what is possible. Only the combination of batched, lean requests and cacheable responses unfolds the full effect. This order is also reflected in a headless performance audit that goes from the frontend code all the way to the cache configuration.

Finally, caching pays off on operating costs: fewer origin requests mean less CPU load, smaller servers and calmer load spikes during promotions. What starts as a pure performance measure thus also relieves the hosting - an effect that becomes clearer as traffic grows and justifies the investment in a clean caching layer beyond pure speed.

This is how your headless shop could look:

Consumer ElectronicsDemo

Elektronik-Shop

This design example shows how a fast, decoupled online shop with clear product navigation and very short response times could look. We build individual headless solutions in which frontend, Store API and caching layer are precisely aligned.
HeadlessStore APICachingPerformance
Discuss your project
Demo
Sources and studies

This article is based on data and documentation from: Shopware Documentation (Store API, Caching Strategy ADR, Release Notes 6.7), Deloitte (Milliseconds Make Millions), web.dev, Google/SOASTA, Conductor (Amazon study), digitalapplied (Headless Commerce 2026) and Cloudflare. The figures mentioned may vary depending on time, shop and measurement method.

Cacheable are typically non-mutating GET endpoints that return no customer-specific data - for example product, listing, category and navigation queries. A route is cacheable when it carries the _httpCache attribute. Cart, account and personalized prices usually stay no-cache, private (Shopware Docs).

The default for cacheable routes is public, max-age=0, s-maxage=1800, stale-while-revalidate=86400, stale-if-error=7200. s-maxage controls shared caches (30 min fresh), max-age=0 forces the browser to revalidate, SWR serves stale content for up to 24 hours instantly and refreshes in the background. The values can be adjusted per route.

Instead of firing a separate call per product, fetch related data via associations and trim the response with includes - ideally in a single batched request. In Nuxt, useAsyncData/useFetch additionally deduplicate parallel calls. This typically reduces the number of calls considerably.

HTTP caches usually store only GET responses. To make queries with complex criteria cacheable, the criteria can be passed as a compressed string in the GET parameter _criteria (JSON, gzip, base64url). POST requests are typically not cached.

Briefly yes - and that is intended. SWR serves the last response immediately after freshness expires and refreshes it in the background, so the next visitor already sees the current version. For fast-moving content you should deliberately set s-maxage and the SWR window shorter, or not cache the route.

Yes. Even a reverse proxy in front of the Shopware origin benefits from the Cache-Control headers and noticeably relieves the PHP and database layers. A globally distributed edge network amplifies the effect but is not a prerequisite. The exact effect depends on traffic, cache strategy and shop structure.

Tags:#Store API#Caching#Headless#Performance#Shopware