Skip to main content

Caching Shopify API data with Hydrogen and Oxygen

Hydrogen and Oxygen provide built-in caching to speed up Hydrogen storefronts. The caching API is based on the web-standard Cache-Control API.

By default, Hydrogen automatically caches Storefront API data when using Hydrogen’s built-in API client. You can customize or disable caching behavior for every request. You can optionally extend Hydrogen’s built-in utilities to cache data from third-party APIs.

Customer Account API data is never cached, because it’s personalized for each user.


Hydrogen includes recommended caching strategies to help you determine which cache control header to set. The following table lists the available caching strategies and their associated cache control headers and cache durations:

Caching strategyCache control headerCache duration
CacheShort()public, max-age=1, stale-while-revalidate=910 seconds
CacheLong()public, max-age=3600, stale-while-revalidate=828001 day
CacheNone()no-storeNo cache
CacheCustom()Define your own cache control headerCustom

Anchor to Default caching strategyDefault caching strategy

By default, each sub-request applies a strategy with the following cache options, which revalidates data after one second and caches it for up to one day:

public, max-age=1, stale-while-revalidate=86399

You can configure the caching strategy for Storefront API data by passing a cache option with your query.

The following simplified example shows a component that displays product titles. Because titles don't change often, it caches the data using the CacheLong() strategy.

Caching product data with Hydrogen

/app/routes/products/$productHandle.jsx

import {useLoaderData} from '@shopify/remix-oxygen';

export async function loader({params, context}) {
const {handle} = params;
const {storefront} = context;

const {product} = await storefront.query(PRODUCT_QUERY, {
variables: {handle},
// Product titles change less often, so they can be cached with CacheLong().
cache: storefront.CacheLong()
});
return {product};
}

export default function Product() {
const {product} = useLoaderData();
return (
<h1>{product.title}</h1>
)
}

const PRODUCT_QUERY = `#graphql
product(handle: $handle) {
id
title
}`
import {useLoaderData} from '@react-router';
import type { LoaderArgs } from '@shopify/remix-oxygen';
import type { Product } from '@shopify/hydrogen/storefront-api-types';

export async function loader({params, context}: LoaderArgs) {
const {handle} = params;
const {storefront} = context;

const {product} = await storefront.query(PRODUCT_QUERY, {
variables: {handle},
// Product titles change less often, therefore CacheLong().
cache: storefront.CacheLong()
});
return {product};
}

export default function Product() {
const {product} = useLoaderData<typeof loader>();
return (
<h1>{product.title}</h1>
)
}

const PRODUCT_QUERY = `#graphql
product(handle: $handle) {
id
title
}
`

Anchor to Prevent caching customer-specific dataPrevent caching customer-specific data

Customer Account API data is automatically excluded from caching, but the Storefront API customer query also returns personalized data. Because the Storefront API client caches responses by default, you need to explicitly disable caching at both the subrequest and full-page levels to avoid leaking one customer's data to another.

Anchor to Disable subrequest cachingDisable subrequest caching

Pass CacheNone() to the Storefront API query to prevent the customer data response from being stored in the subrequest cache:

Disabling subrequest caching for customer data

/app/routes/account.jsx

import {useLoaderData} from '@shopify/remix-oxygen';

export async function loader({context}) {
const {storefront} = context;
const {customer} = await storefront.query(CUSTOMER_QUERY, {
variables: {
customerAccessToken: context.session.get('customerAccessToken'),
},
cache: storefront.CacheNone(),
});
return {customer};
}

export default function Account() {
const {customer} = useLoaderData();
return <h1>Welcome, {customer.firstName}</h1>;
}

const CUSTOMER_QUERY = `#graphql
query CustomerDetails($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
firstName
lastName
email
}
}`;
import {useLoaderData} from '@shopify/remix-oxygen';
import type {LoaderArgs} from '@shopify/remix-oxygen';

export async function loader({context}: LoaderArgs) {
const {storefront} = context;
const {customer} = await storefront.query(CUSTOMER_QUERY, {
variables: {
customerAccessToken: context.session.get('customerAccessToken'),
},
cache: storefront.CacheNone(),
});
return {customer};
}

export default function Account() {
const {customer} = useLoaderData<typeof loader>();
return <h1>Welcome, {customer.firstName}</h1>;
}

const CUSTOMER_QUERY = `#graphql
query CustomerDetails($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
firstName
lastName
email
}
}`;

Anchor to Disable full-page cachingDisable full-page caching

Using CacheNone() on the subrequest prevents caching the API response, but Remix can still cache the rendered HTML response. To prevent shared caches and Oxygen's full-page cache from storing the page, set a Cache-Control header on the loader response. You can choose between two approaches depending on whether you want the browser to cache the response for the individual user:

  • no-store — Disables all caching entirely. No cache, including the user's browser, stores the response. Use this when the data changes frequently or when you don't want any cached version to exist.
  • private, max-age=<seconds> — Prevents shared caches (Oxygen, CDNs, proxies) from storing the response, but allows the user's own browser to cache it for the specified duration. Use this when you want to improve performance for the same user navigating back to the page without risking data leakage to other users.
Caution

If you use a public Cache-Control header or omit the header entirely on routes that return customer-specific data, the rendered HTML page could be cached by shared caches and served to other users, exposing personal information such as names, emails, and order history.


Anchor to Custom caching strategiesCustom caching strategies

If you don't want to use the caching strategies built into Hydrogen, then you can create your own with the CacheCustom() function. This function accepts options compatible with the Cache-Control API.

The following strategy directs clients to revalidate the cached data when it’s no longer fresh, not to transform the data, and to consider it fresh for a maximum of 30 seconds:

Example custom caching strategy

storefront.CacheCustom({
mode: 'must-revalidate, no-transform',
maxAge: 30,
})
storefront.CacheCustom({
mode: 'must-revalidate, no-transform',
maxAge: 30,
})

Hydrogen and Oxygen caching strategies are compatible with the HTTP Header Cache-Control API, with a small number of exceptions.

The following options are available when creating a custom caching strategy with CacheCustom():

OptionTypeDescription
modeStringOne or more comma-separated directives for how caches should handle response data. Accepts public, private, no-store, must-revalidate, and no-transform.
maxAgeNumberThe length of time, in seconds, to cache the response.
staleWhileRevalidateNumberThe length of time, in seconds, to serve a stale response while fetching a fresh one in the background.
sMaxAgeNumberThe length of time, in seconds, that proxies or CDNs can store the response.
staleIfErrorNumberThe length of time, in seconds, for the browser to serve a cached response instead when it receives a 5xx error.

Note thatstaleIfError is ignored when caching sub-requests. Instead, use staleWhileRevalidate to return stale data if errors are thrown during the revalidation period.

Anchor to Cache-Control API exceptionsCache-Control API exceptions

The no-cache directive isn't supported by Oxygen because it instructs the browser not to use the cached data until the server returns a 304 (Not Modified) status from server. However, Oxygen doesn't return the 304 response status code, so this directive has no effect.


Was this page helpful?