How stories are cached in the Content Delivery API

Understanding and optimizing the caching system in Storyblok is crucial to reduce the number of API requests and deliver content faster to your users.

It also has a critical impact on the rate limit, see more info about this in our official documentation.

How caching works in Storyblok

Each space has a version parameter, which is a Unix timestamp that is updated every time you publish new content. Thus, the parameter always reflects the most recent update to the public version of the space.

You can see the latest version value for your space by making an API call to the spaces endpoint.

Example Request
        
      GET https://api.storyblok.com/v2/cdn/spaces/me?token=[YOUR_TOKEN]
    
Example Response
        
      {
  "space": {
    "id": 123456,
    "name": "your-space",
    "domain": "https://your-space.blok/",
    "version": 1735645795 // this is the latest version
  }
}
    

The stories endpoint also exposes the version, which is called cv (cache version) and by default is equal to the version parameter of the spaces endpoint.

Example Request
        
      GET https://api.storyblok.com/v2/cdn/stories?token=[YOUR_TOKEN]
    
Example Response
        
      {
  "stories": [...], // your stories
  "cv": 1735645795, // this is the latest version
  "rels": [],
  "links": []
}
    

The cv is also a query parameter that is used to retrieve the stories. If it's not present in the API request, the Storyblok backend performs an HTTP redirect to the same endpoint with the latest available cv value added to the URL.

Example redirect
        
      GET https://api.storyblok.com/v2/cdn/stories?token=[YOUR_TOKEN]
# => 301 Redirect
GET https://api.storyblok.com/v2/cdn/stories?cv=1735645795&token=[YOUR_TOKEN]
    

Finally, the response to this call is cached in Storyblok's CDN, which means that any new call with the cv parameter equal to the latest available will immediately serve the content from the cache without affecting the API request count.

That's why it's critical to retrieve the version value and use it as the cv query parameter in any subsequent API calls to the stories endpoint. This maximizes the cache usage, speeds up the response time, and significantly reduces the number of API calls that count against Storyblok's rate limit.

hint:

The most efficient way to get the version number is to call the /spaces/me/ endpoint.

When a new story is published, the version value is updated, but the previous value is still cached in the CDN. To get the latest content, you will need to use a cv value that is up to date with the latest version.

Example Response
        
      // GET https://api.storyblok.com/v2/cdn/stories?cv=1735645795&token=[YOUR_TOKEN]
// Will still fetch the previous cached version
{
  "stories": [...],
  "cv": 1735645795, // this is the previous version
  "rels": [],
  "links": []
}

// GET https://api.storyblok.com/v2/cdn/stories?cv=1735815318&token=[YOUR_TOKEN]
// Will fetch the most recent version
{
  "stories": [...],
  "cv": 1735815318, // this is the latest version
  "rels": [],
  "links": []
}
    

Fetching, storing, and invalidating the cv parameter used for API calls should be part of your application architecture. You can use cron jobs, custom events, or Storyblok's webhooks to know when to invalidate your stored cv and retrieve a new one.

Storyblok SDKs and official JS client

If you use a JavaScript framework to interact with Storyblok, you will most likely want to use one of our official SDKs.

They are based on the JS client, designed to handle the cv parameter in a way that, by default, helps you keep the number of API calls to a minimum. To get fresh content on your site as soon as you publish it, it's important that you understand how version caching works in the JS client, especially in the context of a server-side rendered (SSR) application or a long-running process.

How the JS client uses the cv param

When you start your JavaScript application, a new instance of the JS client is created. When you make your first API call to the /stories/ endpoint, the JS client takes the version number in the response and stores it in memory.

This way, it will reuse that value for all subsequent calls unless you provide a custom cv parameter that overrides its cached value.

Example overriding the cv param
        
      import StoryblokClient from "storyblok-js-client";

const Storyblok = new StoryblokClient({
  accessToken: "YOUR_TOKEN",
});

const response = await Storyblok.getStory("home", {
  version: "published",
  cv: '1735815318', // this will be cached by the client
});
    

If you now make a call to the same endpoint without providing the cv value, the cached value will be used.

Example overriding the cv param
        
      // Will result in GET /stories/home?cv=1735815318&version=published&token=[YOUR_TOKEN]
// because the JS client previously stored the cv value
const response = await Storyblok.getStory("home", {
  version: "published",
});
    

If you never pass a custom cv parameter, the behavior is the same as described above for HTTP calls: behind the scenes, a redirect is performed with the most recent cv value, and the JS client stores that value.

This also happens in the SDKs. In the React SDK, for example, this is equivalent to the code we suggest in our documentation and playgrounds.

Example in a Next.js component
        
      export async function fetchData() {
  const sbParams: ISbStoriesParams = { version: 'published' };
  const storyblokApi: StoryblokClient = getStoryblokApi();
  return storyblokApi.get(`cdn/stories/home`, sbParams);
}
    

That's why if your application fetches a story without passing the cv, the version of that story and any other story fetched after that call will be the same as the first call for the entire lifetime of the JS client in memory.

warn:

In the case of some server-rendered applications (like Next.js) the lifetime of the JS client instance may be longer than you expect, and may actually persist until you restart the server. That's why it's important to use webhooks or custom logic to revalidate the cache.

How to clear the JS client's cache

The easiest way to clear the JS client's cache is to call the flushCache() method, which flushes the client's in-memory cache.

JS client flush cache
        
      await Storyblok.flushCache();
    

Another method that actually clears the cache is to call the /stories/ endpoint with the version: 'draft' parameter. Getting the draft content will effectively flush the client cache, although this is only for preview or development environments.

How to always get fresh, uncached content

If you don't want to manage the version number, and want always to fetch the most recent content, simply passing a cv parameter equal to the current time will effectively get fresh and uncached content.

Get uncached content
        
      const response = await Storyblok.getStory("home", {
  version: "published",
  cv: Date.now(),
});
    

This will "bypass" the CDN cache and always hit the Storyblok backend, because the requested version will always be newer than the space latest version. This also means that any call made with this cv value will count against our API rate limit, exactly as a raw call to the endpoint GET /cdn/stories?token=[YOUR_TOKEN]

To summarize

The key aspects of Storyblok's caching system include:

  1. Version Control
    • Each content update generates a new version timestamp
    • The version is exposed as cv (cache version) in API responses
    • Get the latest version via /spaces/me endpoint
  2. Cache Optimization
    • CDN caching reduces API calls and improves performance
    • Include cv parameter in requests to maximize cache usage
    • Automatic redirects add the latest version if cv is missing
  3. Implementation Strategy
    • JS client automatically handles version caching
    • In server-side rendered applications, store your own version somewhere and use webhooks or cron jobs to revalidate it to avoid the risk of outdated content
    • Use the current timestamp as cv to bypass caching when fresh content is needed and API count is not an issue