---
title: 'Storefront: Understanding HTTP Caching | Shopware Community Hub'
description: >-
  Learn how Shopware’s HTTP cache works, how cache keys and invalidation behave,
  and how dynamic content can be handled using ESI and cache variation
  strategies.
canonical_url: 'https://hub.shopware.com/learn/unit/storefront-understanding-http-caching'
---

# Storefront: Understanding HTTP Caching

<LearningObjectives>

- Build a mental model of how Shopware's HTTP cache works.
- Understand when and why storefront routes are cacheable.
- Differentiate between full-page caching and ESI fragment rendering.
- Understand how cache variants (e.g., `sw-cache-hash`) affect content delivery.
- Decide when to use TTL vs. tag-based invalidation.

</LearningObjectives>

# Storefront: Understanding HTTP Caching

Shopware storefront pages combine rich commerce data, server-rendered Twig, dynamic customer context, and many reusable components. HTTP caching helps deliver these pages quickly and at scale while keeping the rendered output correct for the current context.

The main question is not “How do we make a slow page fast with cache?” The question is: How can Shopware reuse safe page responses while still rendering dynamic content correctly?

In this learning unit, you will build a structured understanding of:

- When a storefront response is considered cacheable.
- How cache variants are created and why `sw-cache-hash` exists.
- How TTL and cache tags control freshness.
- How dynamic context can be handled using full-page caching, cache variation, or ESI-based fragment rendering.

## How HTTP Caching Works in the Storefront

In a typical storefront setup, three layers are involved in handling a request:

1. The Browser (client): Sends the HTTP request and receives the HTML response.
2. The HTTP Cache Layer (optional): Stores and serves cached responses if available.
3. Shopware (Symfony): Executes the storefront controller, runs business logic, and renders Twig templates.

The HTTP cache layer sits between the browser and Shopware. If a cached response exists and is still valid, it can return that response directly without calling the Shopware application again.

<Callout title="Note" type="info">

This “HTTP cache layer” can be provided in two ways:

- **Built into the application** (Symfony HTTP cache/App Cache used by Shopware).
- **As an external reverse proxy** (e.g. Varnish/Fastly) in front of Shopware (common in production).

In this learning unit, we focus on **how Shopware marks responses as cacheable and how it varies/invalidates them**. Whether the cached response is served by the built-in layer or an external proxy follows the same core rules (cache headers, cache key/hash, tags/TTL).

</Callout>

Shopware's HTTP cache stores **full HTTP responses**. That means caching is primarily controlled by:

- Response headers (e.g., `Cache-Control`).
- The cache key (including `sw-cache-hash`).
- Cache tags for invalidation.

Only `GET` routes that are explicitly marked as cacheable are stored in the HTTP cache. The following example shows a storefront controller route that is explicitly marked as cacheable.

**Controller route example:**

```php
#[Route(
    path: '/example',
    name: 'frontend.example.page',
    methods: ['GET'],
    defaults: ['_httpCache' => true]
)]
```

Twig renders HTML on the server side. Shopware caches the rendered HTML response, not the individual Twig blocks.

```txt
┌─────────────────────────────────────────────────────────────────┐
│                   SHOPWARE HTTP CACHE FLOW                      │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────┐
│ Browser Request │ ← GET /product/example
└─────────────────┘
         │
         ▼
┌──────────────────────────────────────────┐
│ HTTP Cache Layer                         │
│ (App Cache / Reverse Proxy e.g., Varnish)│
└───────┬──────────────────────────────────┘
        │ 1. Check cache key 
        │   (URL + relevant cookies/headers, incl. sw-cache-hash) → selects cache variant
        │ 
        │──────────────> Cache HIT 
        │                Return cached response 
        │ 
        ▼
    Cache MISS   
        │
        ▼
┌────────────────────────────────┐
│ Shopware Application (Symfony) │
│ ┌───────────────────────┐      │
│ │ Controller            │      │
│ │ -> Load Data          │      │
│ │ -> Render Twig        │      │
│ │ -> Collect cache tags │      │
│ └───────────────────────┘      │
└─────────┬──────────────────────┘
          │
          ▼
┌──────────────────────────────────┐
│ Store Response in HTTP Cache     │
│ - HTML output                    │
│ - Headers (Cache-Control)        │
│ - Cache key (incl. sw-cache-hash)│
│ - Cache tags                     │ 
└─────────┬────────────────────────┘
          │
          ▼
┌─────────────────────┐
│ Response to Browser │
└─────────────────────┘
```

### When is a Route Cacheable?

For a storefront response to be stored in the HTTP cache, several baseline conditions must be fulfilled:

- **HTTP method**: Only `GET` requests are considered cacheable.
- **Route opt-in**: the route must be explicitly marked with `defaults: ['_httpCache' => true]`.
- **HTTP cache enabled**: the HTTP cache layer must be enabled in your environment.
- **Cache policy/headers**: The response must allow caching (i.e., it must not be forced to `private` / `no-store`). In shared-cache scenarios, `Cache-Control: public` is the typical baseline.

If any of these conditions is not met, the response will still render correctly, but it won’t be stored (or served) from the HTTP cache.

<Callout title="Important" type="warning">

“Cacheable route” does not automatically mean “safe for everyone.” A route must only output content that is correct for all customers that share the same cache key variant. If customer-specific data leaks into a shared cache variant, you risk serving the wrong content.

</Callout>

**Typical ways to disable caching (intentionally):**

- Do not set `'_httpCache' => true` on the route of the storefront controller.
- Set cache headers to `private` / `no-store` for the response (e.g., for fragments that must always be fresh).

If a route is cacheable and all conditions are fulfilled, the HTTP cache stores a cache entry.

### What is Stored in a Cache Entry?

When a storefront response is stored in the HTTP cache, Shopware does not only store the HTML body. A cache entry typically contains:

- The rendered HTML response (the final output of Twig).
- Response headers (e.g., `Cache-Control`).
- The **cache key**, which uniquely identifies this variant of the response.
- Cache tags, which are later used for invalidation.

The cache key is especially important. It ensures that different variants of the same route (for example, based on language, currency, or login state) are stored separately.

This means the HTTP cache works on the level of complete HTTP responses, not on individual Twig blocks.

### Cache Hit, Cache Miss, and TTL

A cache hit means the HTTP cache layer (often a reverse proxy) can return a cached response immediately without asking Shopware to render the page again. This is fast, because:

- Shopware does not need to execute the controller.
- No database queries are executed.
- No Twig rendering is performed.
- No business logic runs again.

The response is simply returned from memory (or a very fast cache layer).

Cache miss means no valid cache version exists. Shopware must render the page again and store a new cache entry. This is slower.

TTL (Time To Live) defines how long a cached response is considered valid. For example, the TTL is set to 600 seconds. That means the page stays in the cache for 10 minutes. After the TTL expires, the next request creates a new cache entry.

**Important:**

A cache hit happens only if the cached response is still valid. A response becomes invalid if the TTL expires or the cache entry is actively invalidated

A **high cache hit rate** means that most requests can be served from cache instead of being rendered again. This reduces server load and improves response times.

## Dynamic Content in a Cached Storefront

Storefront pages often contain dynamic content, for example:

- Cart information
- Login state
- Personalized content (e.g., based on a customer's preferences)
- Rule-based content (e.g., based on a customer's group)

At the same time, HTTP caching works best when the same response can be reused for many requests (customers).

This creates a challenge: How do you ensure that pages are cacheable while still rendering dynamic parts correctly?

### Why Dynamic Content is a Problem

HTTP caches are designed to reuse the same response for many requests. Dynamic storefront content breaks that assumption.

If you cache a page that contains **per-customer** or **per-session** output, you can run into two problems:

- **Correctness risk**: The cache may serve HTML that was generated for a different client (content leakage).
- **Low cache efficiency**: If you vary the cache key too much (too many variants), your cache hit rate drops, and caching becomes less valuable.

In Shopware storefronts, dynamic output usually comes from:

- Cart and checkout state (e.g., line items in cart, shipping selection, etc.).
- Authentication state (guest vs. logged-in).
- Rule-based output.

Because of this, developers must decide carefully:

- What can safely be part of a shared cached response?
- What must vary per context?
- What must never be cached?

Let's see how to handle dynamic Twig blocks.

## Handling Dynamic Twig Blocks

When developers say “cache a Twig block” in Shopware, this can be confusing. Twig itself does not cache blocks like a built-in template fragment cache.

<Callout title="Avoid Confusion: Twig {% cache %} vs. Shopware HTTP Cache" type="info">

Twig also has an optional template fragment cache tag: [`{% cache %}`](https://twig.symfony.com/doc/3.x/tags/cache.html).

This is **not the same** as what this learning unit focuses on:

- **Twig `{% cache %}`**: Caches a template fragment inside Twig (server-side), requires additional Twig extensions/packages, and does not automatically integrate with reverse proxy behavior, cache hash variants, or Shopware cache tags.
- **Shopware storefront caching (this learning unit)**: Caches **full HTTP responses** (and optionally **HTTP fragments** via ESI/subrequests) using HTTP semantics (headers, cache key/hash, TTL/tags).

In Shopware projects, the default performance strategy for the storefront is usually **HTTP caching + cache hash + ESI**, not Twig’s fragment cache tag.

</Callout>

In Shopware storefronts, “block caching” usually means one of these three things:

- The block output is cached because the **whole page response** is cacheable.
- The block output is cached (or deliberately not cached) because it is rendered as an **ESI/subrequest fragment** with its own cache rules.
- The block output is cached in multiple variants because the **cache key varies** (e.g., by pagination or sorting parameters on a listing page).

So in Shopware storefronts, caching is typically not controlled directly “inside Twig”; it is primarily controlled by **HTTP caching** and (optionally) **fragment rendering** (ESI/subrequests).

<Callout title="Reminder" type="info">

Twig is rendered on the server side via Symfony. The result of Twig rendering is a full HTML response, which is then returned to the client (browser).

Shopware's HTTP cache stores that response, not the individual Twig blocks.

</Callout>

### Strategy A: Cache the Page

This is the simplest and most common case. If a Twig block only uses data that is safe to cache in the current context, you usually do not need special handling.

In this case the whole page is cacheable, and the Twig block is simply part of that cached HTML response.

A block is safe to cache if its output:

- Does not depend on the current customer.
- Does not depend on the cart.
- Does not change per request.
- Does not depend on a hidden runtime state.

Safe examples are: product name, product description, and category headline. Unsafe examples are: customer name, personalized discount, cart item count, or rule-based content that depends on the session state.

If you want the page to stay cacheable, keep the following in mind:

- Keep the route cacheable.
- Avoid injecting customer-specific output into the Twig template.
- Do not accidentally introduce a state that varies per request.

**Practical rules of thumb:**

- Prefer reading data from `page` object or `context` object. Shopware prepares these objects in a controlled way for the route.
- Avoid accessing arbitrary runtime state (e.g., session or customer data) inside Twig if the page is meant to be cached.
- If a route is meant to be cacheable, make sure it is a `GET` route and has `defaults: ['_httpCache' => true]` (keyword: storefront controller).

**Example:**

Imagine you add a Product Highlight badge in the listing, based on a product custom field.

Since this badge only depends on product data (which is the same for all customers), it can safely remain inside the normal page response and benefit from HTTP caching.

**Mindset behind this strategy:**

Always try to keep the page cacheable unless there is a strong reason not to. Do not introduce dynamic behavior unless it is really necessary.

This strategy leads to better performance because a higher cache hit rate means fewer full page renders on the server.

### Strategy B: Render a Twig Block via ESI

Sometimes a page is mostly static, but one small part is dynamic. In that case, do not disable caching for the entire page. Instead, move the dynamic part into a separate fragment route and include it using ESI.

This allows the main page to stay cacheable, while a small dynamic block can be cached independently.

**What is ESI?**

ESI stands for **Edge Side Includes**.

It is a mechanism used by reverse proxies (e.g., Varnish) to assemble one page from multiple parts:

1. The proxy loads the main HTML page.
2. It detects ESI include tags.
3. It loads each fragment separately.
4. It combines everything into one final HTML response.

The browser only receives the final result.

#### Performance Consideration

However, a common question is whether ESI makes pages load slower.

Sometimes yes, but often no.

ESI adds a “page assembly step” on the reverse proxy: The proxy must fetch the main HTML and the fragments before it can return the final HTML to the browser.

In practice, ESI is usually a win when caches are warm. If the **main page** is cached, it is served very fast. If the **fragments** are cached as well, the proxy can fetch and merge them quickly (often without calling your application at all).

ESI can feel slower when caches are cold or misconfigured. If the main page and several fragments are **cache misses**, the proxy must make extra backend requests. This can increase the time to first byte (TTFB), which means the time until the browser receives the first part of the response.
Also, if fragments have many changing parameters, you create many cache variants and reduce the hit rate.

**Rules of thumb**

Use ESI for a **limited number of small fragments** that really need a different caching strategy than the main page. Do not split a page into many fragments “just because you can”.

#### How ESI Works in Symfony and Shopware

As you already know, Shopware is built on Symfony. Symfony provides fragment rendering, ESI support, and the Twig extension [render_esi](https://symfony.com/doc/current/reference/twig_reference.html#render-esi).

Shopware uses this system as part of its HTTP caching strategy. For example, since Shopware 6.7, header and footer are loaded via ESI/subrequests by default, which improves the cache efficiency.

Think of this concept like this:

- The main page stays cacheable (long TTL, high hit rate).
- The fragment route can be cached with a separate TTL, different cache behavior, or even no caching.

**Twig example (ESI include):**

```twig
{# render a fragment as ESI (falls back to inline rendering if ESI is not available) #}
{{ render_esi(url('frontend.my.fragment', { id: page.product.id })) }}
```

What happens here:

- Symfony generates an ESI include tag (e.g., `<esi:include .../>`).
- If an ESI-capable reverse proxy is present, the fragment is loaded separately and merged into the final HTML.
- If not, Symfony renders the fragment inline as a fallback (as a server-side fragment sub-request during the main request).

In practice, this can mean:

- With no ESI gateway cache: 1 main request + internal fragment sub-requests (e.g., header and footer in Shopware 6.7).
- With an ESI gateway cache (e.g., Varnish): the gateway fetches the fragments as separate backend HTTP requests and assembles the final HTML before it reaches the browser.

This means your code works in both setups.

#### How to Validate ESI in the Browser

ESI is resolved on the server side (by the reverse proxy). This means:

- You will usually **not** see an extra request in your browser network tab for the fragment.
- Your browser receives the final assembled HTML (main page + fragments already merged).

To verify that ESI works, use these practical checks:

1. **Check the HTML response**
   - Open DevTools -> Network -> select the document request.
   - In the **Response** tab, search for `esi:include`.
   - If you still see `<esi:include .../>`, ESI is **not** processed (no proxy, or ESI is not enabled).
   - If you do not see any ESI tags, either the proxy processed them **or** Symfony rendered the fragment inline as a fallback.

2. **Open the fragment directly**
   - Open the fragment route in a new tab, for example `/widgets/my-fragment/123`.
   - You should see only the fragment HTML (not the whole page).
   - Check response headers (e.g., `Cache-Control`) to understand how the fragment is cached.

3. **Advanced: Simulate an ESI-capable proxy**
   - Symfony generates ESI tags only when it detects an ESI-capable gateway cache. Symfony checks for the `Surrogate-Capability` request header containing `ESI/1.0`.
   - You can simulate this with `curl` and compare the HTML output:

```bash
# 1) Normal request (often renders inline if no ESI proxy is detected)
curl -sS https://your-shop.test/some-page | grep -n "esi:include"

# 2) Simulate an ESI-capable gateway cache (ESI tags should be present in the HTML)
curl -sS -H 'Surrogate-Capability: shopware="ESI/1.0"' https://your-shop.test/some-page | grep -n "esi:include"
```

If the second command shows `esi:include` but the first one does not, your application is configured correctly and will output ESI tags when a gateway cache is present.

You do not need this `curl` check for everyday storefront work. It is mainly useful when you debug a reverse-proxy setup or want to understand whether Symfony emits ESI tags for an ESI-capable gateway.

#### Example: Controller Fragment Route

```php
#[Route(
    path: '/widgets/my-fragment/{id}',
    name: 'frontend.my.fragment',
    methods: ['GET'],
    defaults: ['_httpCache' => true]
)]
public function fragment(string $id, SalesChannelContext $context): Response
{
    return $this->renderStorefront('@Storefront/storefront/component/my-fragment.html.twig', [
        'id' => $id,
    ]);
}
```

This route is a normal `GET` route, can be cacheable (with `defaults: ['_httpCache' => true]`), and can have its own TTL or cache rules.

<Callout title="Important" type="warning">

Keep ESI parameters small and simple.

If you pass many parameters (especially values that change often), you create many cache variants and reduce cache hit rate.

</Callout>

Use the ESI strategy when:

- Most of the page is identical for many customers.
- Only a small part depends on the customer state.
- You want to keep the main page highly cacheable.

Do not use ESI for everything. Use it only for a small dynamic section, otherwise you will lose cache efficiency.

### Strategy C: Disable Caching for One Block Without Disabling the Whole Page

Sometimes a small part of the page must always be dynamic and must never be cached (for example: a live cart indicator).
You still want the rest of the page to stay cacheable.

In many storefront projects, the first recommendation is to load this kind of data with JavaScript (client-side) after the cached HTML was delivered. This avoids adding many server-side fragments and keeps the caching model simple.

There are two common approaches:

#### Approach 1: Use a Fragment Route That is not Cacheable

You can render the block via a fragment route but make only that route uncacheable. The main page remains cacheable. To do this: Do not set `defaults: ['_httpCache' => true]` for the fragment route OR explicitly set cache headers like `$response->headers->set('Cache-Control', 'private, no-store');`.

This ensures that the main page can still have a long TTL, the fragment is always freshly rendered and no cached version of the fragment is reused. This is useful, for example, for live cart information or session-based content.

<Callout title="Recommendation" type="info">

Prefer only a small number of server-side fragments per page.

As a rule of thumb, avoid having more than 2–3 ESI/fragment blocks on one page, otherwise you can lose cache efficiency and increase backend load.

</Callout>

#### Approach 2: Render Inline Instead of ESI

Instead of using `render_esi()`, you can render a fragment inline using Symfony Twig extension [render](https://symfony.com/doc/current/reference/twig_reference.html#render):

```twig
{{ render(url('frontend.my.fragment', { id: page.product.id })) }}
```

This means no ESI tag is generated, the fragment is rendered directly during page rendering, and it becomes part of the main response.

**Important:**

Inline rendering is not automatically dynamic; It only changes how the fragment is included. If the main page is cacheable, the inline-rendered content becomes part of that cached response.

Use this strategy when:

- You do not have (or do not want to rely on) an ESI-capable reverse proxy.
- The fragment is cache-safe, and you want it to follow the caching behavior of the **main page**.
- You want the simplest integration and accept that the fragment becomes part of the page response.

<Callout title="Important" type="warning">

Inline rendering does not make a block “uncacheable”. If the main page response is cached, the inline-rendered HTML is cached as well.
If a block must never be cached, use an **uncacheable fragment route** (Approach 1) instead.

</Callout>

Remember that every time you disable caching for a fragment, you reduce performance benefits; Use this strategy only when necessary.

**Mental Model:**

- Use strategy A if you want to cache everything.
- Use strategy B if you want to cache the page but separately cache small parts.
- Use strategy C if you want to cache the page but not cache the fragment.

### Shopware-specific example: Header/Footer via ESI

In Shopware 6.7, header and footer are loaded via ESI/subrequests by default.
If you need to influence them based on the current page, Shopware supports passing scalar values via `headerParameters` / `footerParameters` (as query parameters for the ESI routes).

That means you can keep the main page cacheable while still providing limited context to header/footer.

### Simple Example: Cacheable Page and Dynamic Fragment (ESI)

This short example shows a typical real-world pattern:

- A **cacheable** storefront page route (`_httpCache: true`) that should have a high cache hit rate.
- A small **dynamic** section that is rendered via a **fragment route** and included with `render_esi()`.

The following example demonstrates the complete pattern in isolation.

#### Step 1: Cacheable Storefront Controller

```php
<?php declare(strict_types=1);

namespace Example\Storefront\Controller; // Example namespace

use Shopware\Core\PlatformRequest;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Controller\StorefrontController;
use Shopware\Storefront\Framework\Routing\StorefrontRouteScope;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [StorefrontRouteScope::ID]])]
final class ExamplePageController extends StorefrontController
{
    #[Route(
        path: '/httpcache-demo',
        name: 'frontend.httpcache.demo.page',
        methods: ['GET'],
        defaults: ['_httpCache' => true]
    )]
    public function page(SalesChannelContext $context): Response
    {
        return $this->renderStorefront('@SwagHttpCacheDemo/storefront/page/httpcache-demo/index.html.twig');
    }
}
```

#### Step 2: Twig template: Include Dynamic Fragment via ESI

```twig
{% sw_extends '@Storefront/storefront/base.html.twig' %}

{% block base_content %}
    <h1>HTTP Cache Demo</h1>

    <p>This page can be cached as a full response.</p>

    <h2>Dynamic block (fragment)</h2>
    {# ESI include tags require absolute URLs. Shopware provides rawUrl() for this. #}
    {{ render_esi(rawUrl('frontend.httpcache.demo.fragment')) }}
{% endblock %}
```

If no reverse proxy is configured, `render_esi()` falls back to inline rendering (`render`).

#### Step 3: Fragment Route: Deliberately Uncacheable (Always Fresh)

```php
<?php declare(strict_types=1);

namespace Swag\HttpCacheDemo\Storefront\Controller;

use DateTimeImmutable;
use Shopware\Core\PlatformRequest;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Controller\StorefrontController;
use Shopware\Storefront\Framework\Routing\StorefrontRouteScope;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [StorefrontRouteScope::ID]])]
final class ExampleFragmentController extends StorefrontController
{
    #[Route(
        path: '/widgets/httpcache-demo',
        name: 'frontend.httpcache.demo.fragment',
        methods: ['GET']
        // Note: No "'_httpCache' => true" here on purpose
    )]
    public function fragment(SalesChannelContext $context): Response
    {
        $response = $this->renderStorefront(
            '@SwagHttpCacheDemo/storefront/component/httpcache-demo-fragment.html.twig', 
            [
              'generatedAt' => (new DateTimeImmutable())->format('H:i:s'),
            ]
        );

        // Make the fragment explicitly uncacheable for shared caches
        $response->headers->set('Cache-Control', 'private, no-store');

        return $response;
    }
}
```

```twig
{# storefront/component/httpcache-demo-fragment.html.twig #}
<div class="httpcache-demo-fragment">
    Fragment generated at: {{ generatedAt }}
</div>
```

With this pattern, the main page can be cached aggressively, while the dynamic block stays correct because it is rendered separately.

If you later decide the fragment can be cached, you can make the fragment route cacheable (`_httpCache: true`) and use an appropriate TTL/policy for that fragment.

#### Optional: Make the Fragment Cacheable (With its own TTL)

Below is the same fragment route, but now explicitly marked as cacheable and with a short TTL (demo values).

```php
#[Route(
    path: '/widgets/httpcache-demo',
    name: 'frontend.httpcache.demo.fragment',
    methods: ['GET'],
    defaults: ['_httpCache' => true]
)]
public function fragment(SalesChannelContext $context): Response
{
    $response = $this->renderStorefront('@SwagHttpCacheDemo/storefront/component/httpcache-demo-fragment.html.twig', [
        'generatedAt' => (new DateTimeImmutable())->format('H:i:s'),
    ]);

    // Demo TTL: keep fragment cached for a short time
    $response->setPublic();
    $response->setMaxAge(30); // demo TTL

    return $response;
}
```

## Cache Keys and Invalidation

Caching is not only about storing responses. It is also about answering two core questions:

1. Variant question: Which requests may share the same cached response?
2. Freshness question: When should a cached response be considered outdated?

Shopware addresses these with cache keys and invalidation mechanisms.

### Cache Variants and the `sw-cache-hash`

Shopware uses a cache hash cookie (`sw-cache-hash`) to represent cache-relevant “session state” in a way that also works with the built-in cache and external reverse proxies.

**Example: Guest vs. logged-in customer**

In Shopware 6.7, do not assume that “guest vs. logged-in” is always expressed only as a different `sw-cache-hash` variant. Depending on the route and the current state (e.g., a logged-in customer or a non-empty cart), Shopware may also decide that the response must not be stored or served from the **shared** HTTP cache at all (for example by using non-cacheable cache headers).

Mental model:

- The cache hash is one mechanism to separate cache variants (when a route is cacheable).
- But it is not the only mechanism.
- Shopware may also prevent shared caching entirely for certain states to ensure correctness.

<Callout title="Version Note" type="info">

In newer versions (e.g., 6.8), the cache hash calculation explicitly includes the logged-in status.

This leads to more consistent cache variants between guest and logged-in users and can reduce reliance on non-cacheable responses in some situations.

</Callout>

In the default guest state, the `sw-cache-hash` cookie might not be set yet. As soon as the context differs from the default (e.g., login, cart not empty, different currency/language), Shopware will start using distinct cache variants.

- If two requests are compatible (same `sw-cache-hash`), they share the same cache entry.
- If they are not compatible, the cache key differs and a separate variant is created.

Typical factors that influence cache variants:

- Guest vs. logged-in
- Language and currency
- Tax state (gross/net)
- Matched cache-relevant rules
- Cart state (depending on setup/policy)

Note: Which signals exactly influence the `sw-cache-hash` can be version- and configuration-dependent. Treat the list above as typical factors and verify behavior in your target Shopware version (e.g., by inspecting the `sw-cache-hash` cookie and the response headers).

**Key takeaway**

The cache hash is a performance tool. It keeps responses correct while still allowing shared caching. However, every additional dimension increases the number of variants and can reduce cache hit rates.

### Intentionally Varying Cache Keys

Sometimes the same URL can legitimately produce different HTML outputs depending on context. In Shopware storefronts, cache key variation is typically achieved by:

- The cache hash (recommended for session-like state).
- Query parameters, when you intentionally want separate variants (e.g., pagination or sorting on a listing page).
- Moving dynamic parts into ESI fragments, so the main page stays stable.

<Callout title="Important" type="warning">

Avoid varying the cache by tracking parameters (e.g., `utm_*`, `gclid`). These parameters usually do not change the HTML, but they can create many unnecessary cache variants and reduce your cache hit rate.

</Callout>

**Rule of thumb:**

Prefer a few stable full-page variants and small dynamic fragments over many full-page variants. High stability means high cache hit rates.

### Freshness: TTL vs. Tags

Once a response is cached, it must eventually become outdated. Shopware uses two complementary mechanisms to keep cached responses reasonably fresh:

- **TTL (time-based expiration)**:
   A response expires after a configured time (via caching policies or `Cache-Control`). After expiration, the next request generates a new cache entry.
- **Cache tags (event-based invalidation)**:
   When rendering a cached response, Shopware collects cache tags (usually from Store API calls). If relevant data changes, Shopware invalidates all cached responses carrying those tags. This allows precise invalidation.

### Why Both Exist

In practice, both are used, but not equally for all page types:

- **Detail pages** can often be invalidated precisely via tags.
- **Listing/search pages** usually rely more on TTL, because invalidating “all possible filtered/sorted/paginated listing variants” is not possible at scale.

So if listing pages can be stale for a short time under caching, that is usually expected behavior and should be handled by tuning TTL/policies rather than trying to force perfect per-entity invalidation.

## Debugging and Best Practices

Even with a solid caching strategy, issues can occur. When something behaves unexpectedly, follow a structured debugging approach.

### Debugging: What to Check First

Start by verifying the fundamentals.

**Is the route cacheable?**

- Is it a `GET` request?
- Does the route explicitly set `defaults: ['_httpCache' => true]`?

If not, the response will never be stored.
  
**Do the response headers allow shared caching?**

Check the response headers:

- `Cache-Control` should contain `public`.
- In shared-cache setups, the response must be cacheable for the cache layer (not forced to `private` / `no-store`).
- `Vary` should include headers/cookies that define cache variants.

Incorrect headers are a common reason for unexpected misses.

**Are you actually hitting a cache?**

In reverse proxy setups:

- `Age > 0` usually indicates cached delivery.
- Surrogate key headers (e.g., `Xkey`) help verify tag-based invalidation wiring.

If `Age` stays `0`, the request likely never reaches the shared cache.

### Best Practices

Keep your architecture simple and stable:

- Keep the main page cacheable whenever possible (Strategy A).
- Use ESI/subrequests for small truly dynamic areas (Strategy B).
- If something must never be cached, isolate it as an uncacheable fragment (Strategy C / Approach 1).
- Avoid over-varying: Every new cache-key dimension multiplies variants and reduces hit rate.

### Common pitfalls

Common pitfalls include:

- **Accidentally caching customer-specific output** in a shared cache response (e.g., customer name, cart contents) without correct variation.
- **Over-varying cache keys** (too many dimensions) and losing cache efficiency.
- **Assuming every listing/search update is visible immediately**: Listing pages usually rely on TTL or broader invalidation rules rather than perfect per-entity invalidation for every possible variant.

## Summary

Very well! In this learning unit, you learned how Shopware's HTTP cache works in the storefront. You now understand:

- When a storefront route is cacheable.
- How full-page caching differs from ESI fragment rendering.
- How cache variants are created using the cache key and `sw-cache-hash`.
- When to rely on TTL and when tag-based invalidation is more appropriate.

With this foundational understanding, you are now prepared to reason about caching decisions in your own storefront implementations.
