---
title: Caching | Qwik Router
description: Learn how to handle caching in your Qwik Router app to keep your site as fast as possible.
contributors:
  - steve8708
  - harishkrishnan24
  - maiieul
  - igorbabko
  - mrhoodz
  - mhevery
  - chsanch
  - hamatoyogi
  - Jemsco
updated_at: '2023-10-03T18:53:23Z'
created_at: '2023-05-24T03:52:24Z'
---

import { Note } from '~/components/note/note';

# Caching Responses

Caching responses is critical for keeping your site as fast as possible, both for [pages](/docs/(qwikrouter)/pages/index.mdx) as well as [middleware](/docs/(qwikrouter)/middleware/index.mdx).

A option is to use [stale-while-revalidate](https://web.dev/stale-while-revalidate/) caching for all responses. Note that this means that users will see a cached response even if the server is updated, and only when the user refreshes will they see the updated content.

For instance, we can add an `onGet` export to our root [layout](/docs/(qwikrouter)/layout/index.mdx) (`src/routes/layout.tsx`) like so, to apply good caching defaults site-wide:

```tsx title="src/routes/layout.tsx"
import { component$, Slot } from "@qwik.dev/core";
import type { RequestHandler } from "@qwik.dev/router";

export const onGet: RequestHandler = async ({ cacheControl }) => {
  cacheControl({
    // Always serve a cached response by default, up to a week stale
    staleWhileRevalidate: 60 * 60 * 24 * 7,
    // Max once every 5 seconds, revalidate on the server to get a fresh version of this page
    maxAge: 5,
  });
};

export default component$(() => {
  return (
    <main class="mx-auto max-w-[2200px] relative">
      <Slot />
    </main>
  );
});
```

With the above setup, you will not only have better performance (pages are always served instantly from cache), but you can also have significantly decreased hosting costs, as our server or edge functions only need to run at most once every 5 seconds per page.

## `cacheControl`

Any method that takes a [request event](https://qwik.dev/docs/advanced/request-handling/#request-event) can call `request.cacheControl` to set the cache control headers for the response:

```tsx title="src/routes/layout.tsx"
import type { RequestHandler } from "@qwik.dev/router";

export const onGet: RequestHandler = async ({ cacheControl }) => {
  cacheControl({
    public: true,
    maxAge: 5,
    sMaxAge: 10,
    staleWhileRevalidate: 60 * 60 * 24 * 365,
  });
};
```

If you have default caching set at the root, but want to disable caching for a specific page, you can override this setting using nested layouts.  The below example overrides caching for dashboard pages.

```tsx title="src/routes/dashboard/layout.tsx"
import type { RequestHandler } from "@qwik.dev/router";

// Override caching for /dashboard pages to not cache as they are unique per visitor
export const onGet: RequestHandler = async ({ cacheControl }) => {
  cacheControl({
    public: false,
    maxAge: 0,
    sMaxAge: 0,
    staleWhileRevalidate: 0,
  });
};

```

You can see the full [API reference](https://qwik.dev/api/qwik-router-middleware-request-handler/) of options you can pass to `request.cacheControl`.

## When not to cache

Caching is generally beneficial, but not right for every page all the time. If your site has URLs that will show different content to different people — for example, pages exclusive to logged-in users or pages that show content based on a user's location — you should avoid using cache-control headers to cache these pages. Instead, render the content of these pages on the server side on a per-visitor basis.

For high traffic pages that look the same to everyone, such as a homepage, caching is great for enhancing performance and reducing cost. For pages specifically for logged in users that may have less traffic, it may advisable to disable caching.

You can conditionally change cache behaviors with any logic you like:

```tsx title="src/routes/layout.tsx"
import type { RequestHandler } from "@qwik.dev/router";

export const onGet: RequestHandler = async ({ cacheControl, url }) => {
  // Only our homepage is public and should be CDN cached. Other pages are unique per visitor
  if (url.pathname === '/') {
    cacheControl({
      public: true,
      maxAge: 5,
      staleWhileRevalidate: 60 * 60 * 24 * 365,
    });
  }
};
```

## CDN Cache Controls

For even more control on your caching strategy, your CDN might have another layer of cache control headers.

The `cacheControl` convenience method can receive a second argument (set to `"Cache-Control"` by default). You can pass in any string value specific to your CDN such as "CDN-Cache-Control", "Cloudflare-CDN-Cache-Control", "Vercel-CDN-Cache-Control", etc.

```tsx
cacheControl({
  maxAge: 5,
  staleWhileRevalidate: 60 * 60 * 24 * 365,
}, "CDN-Cache-Control");
```

### Missing Controls

Some CDNs (such as Vercel Edge) may strip some of your "Cache-Control" headers.

[On Vercel's documentation:](https://vercel.com/docs/edge-network/caching#cdn-cache-control)
<Note>If you set Cache-Control without a CDN-Cache-Control, the Vercel Edge Network strips s-maxage and stale-while-revalidate from the response before sending it to the browser. To determine if the response was served from the cache, check the [x-vercel-cache](https://vercel.com/docs/edge-network/caching#x-vercel-cache) header in the response.</Note>

If your CDN, such as Vercel Edge, automatically removes certain cache control headers and you wish to implement caching strategies like "stale-while-revalidate" or "s-maxage" in the browser, you can specify an additional cacheControl:

```tsx title="src/routes/layout.tsx"
import type { RequestHandler } from "@qwik.dev/router";

export const onGet: RequestHandler = async ({ cacheControl }) => {
    // If you want the browser to use "stale-while-revalidate" or "s-maxage" Cache Control headers, you have to add the second cacheControl with "CDN-Cache-Control" or "Vercel-CDN-Cache-Control" on Vercel Edge 
    cacheControl({
      staleWhileRevalidate: 60 * 60 * 24 * 365,
      maxAge: 5,
    });
    cacheControl({
      maxAge: 5,
      staleWhileRevalidate: 60 * 60 * 24 * 365,
    }, "CDN-Cache-Control");
};
```

### Caching bundles and assets

See [Cache Headers](/docs/deployments/#cache-headers) for more information.

## SSR Caching

Qwik Router includes a built-in in-memory SSR cache that can serve pre-rendered HTML without re-executing components. This is controlled through the `eTag` and `cacheKey` exports (or via `routeConfig`).

### `eTag`

The `eTag` export defines an [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) for the page. When a browser sends an `If-None-Match` header matching the ETag, the server responds with `304 Not Modified` (empty body) instead of sending the HTML.

It can be:

- **A static string** -- e.g. a deployment version or content version.
- **A function** receiving `DocumentHeadProps` -- compute the ETag from route data (loaders, params, etc.). Return `null` to skip the ETag for this request.

Qwik normalizes eTag values by stripping weak-form `W/` prefixes, quotes, and forbidden chars. Values that normalize to an empty string are treated as absent.

Qwik sends all eTags as "strong", for a simpler API. This only makes a difference with ranged HTTP requests, which are not relevant for SSR or route loader data.

An eTag *must* always change when the content changes, and ideally should not change when the content is the same.

```tsx title="src/routes/product/[id]/index.tsx"
import type { RouteConfig } from '@qwik.dev/router';

// Static eTag (e.g., for content that changes only on deployment)
export const routeConfig: RouteConfig = {
  eTag: 'v1.2.3',
};

// Dynamic eTag based on data
export const routeConfig: RouteConfig = ({ resolveValue }) => {
  const product = resolveValue(useProduct);
  return {
    eTag: `product-${product.id}-${product.updatedAt}`,
  };
};
```

> **Note**: Markdown (`.md`) files automatically get an eTag based on a content hash. MDX files (`.mdx`) do not, since they can contain dynamic JavaScript.

### `cacheKey`

The `cacheKey` export enables in-memory SSR caching. When set, rendered HTML is cached and served on subsequent requests with the same cache key, completely bypassing SSR.

- `true`: uses the default cache key — `${status}|${eTag}|${pathname}` when an `eTag` is set, otherwise `${status}|${pathname}`.
- Function `(requestEv, eTag) => string | null`: return a custom key, or `null` to skip caching this request. The second argument is the normalized eTag (or an empty string when none was provided or it normalized to an empty string).

```tsx title="src/routes/product/[id]/index.tsx"
import type { RouteConfig } from '@qwik.dev/router';

export const routeConfig: RouteConfig = {
  eTag: 'v1',
  cacheKey: true, // cache using default key format
};

// Custom key — vary on a `lang` cookie
export const routeConfig: RouteConfig = {
  cacheKey: (ev, eTag) => {
    const lang = ev.cookie.get('lang')?.value ?? 'en';
    return `${ev.url.pathname}|${lang}|${eTag ?? '*'}`;
  },
};
```

`cacheKey` can be set without an `eTag`. In that case the first SSR streams as usual (no ETag header on the first response), and the cache write hashes the rendered HTML so subsequent hits serve the entry with a stable ETag and can answer conditional requests with `304`.

The cache uses LRU eviction with a default capacity of 50 entries. You can configure this in your server entry:

```tsx title="src/entry.server.tsx"
// Set before any request handling
globalThis.__SSR_CACHE_SIZE__ = 100; // or 0 to disable
```

### Clearing the cache

Use `clearSsrCache()` to invalidate cached pages after deployments or data changes:

```tsx
import { clearSsrCache } from '@qwik.dev/router/middleware/request-handler';

// Clear a specific entry
clearSsrCache('200|v1|/products/123');

// Clear the entire cache
clearSsrCache();
```

### Layout-level caching

Layouts can define `eTag` and `cacheKey` via `routeConfig` to apply caching to an entire subtree. Pages can override these values in their own `routeConfig`:

```tsx title="src/routes/blog/layout.tsx"
import type { RouteConfig } from '@qwik.dev/router';

// All blog pages use SSR caching by default
export const routeConfig: RouteConfig = {
  cacheKey: true,
};
```

See the [routeConfig documentation](/docs/(qwikrouter)/pages/index.mdx#routeconfig-export) for the full resolution rules.

## Loader caching

`routeLoader$` accepts an `eTag` (string or function) and a `cacheKey` option that mirror the SSR-side ones, but applied to the loader's JSON response.

Qwik normalizes explicit loader eTag values by stripping weak-form `W/` prefixes, quotes, and forbidden chars, then sends a strong `ETag` header. Values that normalize to an empty string are treated as absent.

```tsx
import { routeLoader$ } from '@qwik.dev/router';

export const useProduct = routeLoader$(
  async ({ params }) => {
    return await db.product.find(params.id);
  },
  {
    // Auto-hashed eTag derived from the serialized response on cache miss; stored alongside the
    // body so future hits serve the same validator and answer `If-None-Match` with `304`.
    cacheKey: true,
  }
);
```

- `eTag` (string | function): explicit validator. Set before the loader runs, so a matching `If-None-Match` short-circuits with `304` and the loader is skipped entirely.
- `cacheKey` (`true` | function): enables an in-memory cache for the serialized JSON response. Default key: `${pathname}|${filteredSearch}|${loaderId}`, suffixed with `|${eTag}` when an explicit eTag is set.

Loader `cacheKey` callbacks receive the loader-filtered request event. They can read the filtered search string from `ev.url.search`.

On a cache miss the loader runs, the serialized response is stored alongside its eTag (the explicit one if provided, or an auto-hash of the serialized body), then sent. On a cache hit the stored `{ eTag, data }` pair is served directly — `If-None-Match` is checked against the stored eTag and a `304` is returned when it matches. Redirects and errors are never cached.

Clear the loader cache the same way as the SSR cache:

```tsx
import { clearLoaderCache } from '@qwik.dev/router/middleware/request-handler';

clearLoaderCache(); // clear all
clearLoaderCache('/products/123||hash'); // clear one entry
```
