Content Delivery API

Integrating cache tags in your project

Whatever your stack, an integration comes down to two things: marking each cached artifact with the tags that the Content Delivery API returned, and purging those tags when the invalidation webhook fires. How you do them depends on what sits between your code and your visitors.

This page starts with the general case, any server behind any CDN, which is the simplest to reason about and the clearest way to see the mechanism. The second half covers popular frameworks and hosting platforms, where the picture is less uniform.

Any server, any CDN

If your application can set custom HTTP headers on a per-page basis, then regardless of language or framework you can use cache tags by placing a CDN that supports tag-based invalidation on top of it.

What is tag-based cache invalidation?

Tag-based cache invalidation is a method where keywords (tags) can be assigned to cached pages. This technique is provided by all the major content delivery services such as Netlify, Fastly, Bunny and Cloudflare. The mechanism:

  • Assign tags: When your application delivers a page, it lists a series of tags in a response header (the header's name depends on the CDN). Each tag labels a piece of the content on that page.

  • Caching: The CDN stores the response under its primary cache key, the URL, together with the associated tags.

  • Purging: When content behind a tag changes, you can easily drop every cached item carrying that tag in a single call.

Different services name the same concept differently. Fastly calls cache tags "Surrogate Keys". The header your application uses to declare the tags varies too: Netlify and Cloudflare read Cache-Tag, Bunny reads CDN-Tag. Netlify also accepts Netlify-Cache-Tag, which is preferable there because Netlify strips it from the response before it reaches the browser. What this documentation calls "cache invalidation", other services call "cache purge".

Check your CDN's documentation for the exact details, format, and limitations.

The whole integration, end to end

The integration is small enough to show in full. The example below uses Hono as the server and Fastly as the CDN, but nothing in it is specific to either: the same two response headers and the same purge call apply to Express, Fastify, Rails, Laravel, or anything else that can set a header.

Tagging a response. Ask the Content Delivery API for cache tags, then forward them to the CDN:

import { Hono } from 'hono';
import { rawExecuteQuery } from '@datocms/cda-client';
const app = new Hono();
app.get('/posts/:slug', async (c) => {
const [data, response] = await rawExecuteQuery(POST_QUERY, {
token: process.env.DATOCMS_CDA_TOKEN,
returnCacheTags: true,
variables: { slug: c.req.param('slug') },
});
const cacheTags = response.headers.get('x-cache-tags');
if (cacheTags) {
c.header('Surrogate-Key', cacheTags);
c.header('Surrogate-Control', 'max-age=31536000');
}
return c.html(renderPost(data));
});

Two details in there:

  • X-Cache-Tags is space-separated, and so is Fastly's Surrogate-Key: the value can travel verbatim. CDNs that expect a comma-separated Cache-Tag need cacheTags.split(' ').join(',') instead.

  • Surrogate-Control tells the CDN how long to keep the response, and Fastly strips it before the response reaches the browser. A one-year lifetime on the CDN is therefore safe, since cache tags rather than time expire the entry. Add a regular Cache-Control header, with a much shorter lifetime, if you also want visitors' browsers to cache the page.

Purging on content change. The webhook endpoint receives the tags and hands them to the CDN's purge API:

app.post('/api/invalidate-cache', async (c) => {
if (c.req.header('authorization') !== `Bearer ${process.env.WEBHOOK_TOKEN}`) {
return c.json({ success: false }, 401);
}
const { entity } = await c.req.json();
const { tags } = entity.attributes;
const response = await fetch(
`https://api.fastly.com/service/${process.env.FASTLY_SERVICE_ID}/purge`,
{
method: 'POST',
headers: {
'fastly-key': process.env.FASTLY_KEY,
'content-type': 'application/json',
},
body: JSON.stringify({ surrogate_keys: tags }),
},
);
return c.json({ success: response.ok }, response.ok ? 200 : 502);
});

The integration ends there: two response headers on the way out, and one POST when content changes.

Popular frameworks and hosting platforms

Some frameworks offer their own caching layer, with helpers that work across hosting providers instead of raw HTTP headers. When one is available it can be the more idiomatic path, but frameworks differ in how well they carry a set of cache tags from end to end, and that difference is worth knowing before you commit to a stack.

We cannot document every framework and hosting combination. We maintain guides for the setups people ask us about most, and for the ones we are most comfortable recommending.

What makes a stack a good fit

Three questions tell you most of what you need to know about a combination we haven't documented:

  • Does it carry all your tags? A Content Delivery API response can carry up to 500 cache tags, and the ceiling can come from either the hosting platform or the framework. Among platforms, Cloudflare's Cache-Tag header holds roughly 1,000 and Netlify accepts exactly 500, both above what we emit, as is Fastly's Surrogate-Key; Vercel caps a cached response at 128 tags, well below our maximum. Among frameworks, Next.js applies that same figure one layer up: it associates at most 128 tags with each fetch(), so the limit travels with the framework even when you host it elsewhere.

  • Does it need extra infrastructure? When tags travel verbatim from our response header into the CDN's header, nothing else is required. When they don't fit, you need somewhere to store the mapping between what you tagged and what we invalidate, which means a database in the request path.

  • How fast can it invalidate? Purge APIs cap the tags per request and rate-limit the calls. Cloudflare accepts 100 tags per purge request on every plan, at 5 requests per minute on Free up to 50 per second on Enterprise. Netlify allows a purge twice every five seconds, per tag or per site. Vercel's bulk endpoint accepts 16 tags per call, the tightest of the three. Since a bulk publish can produce several hundred tags in one webhook delivery, plan on chunking and retrying.

Where the friction shows up

Next.js is the clearest example of the second question mattering. Since a fetch() accepts at most 128 tags, you cannot hand DatoCMS tags to Next.js directly. Our guide works around this by tagging each query with a single synthetic identifier and keeping a "query ID to cache tags" mapping in a persistent database, written whenever the cache is filled and read whenever the webhook fires. The approach works, we document it in full, and a starter project comes with it, but you will run and operate that database, and it sits in the invalidation path. The two costs belong to different layers: you pay for the mapping layer because of the framework, while the 128-tag ceiling is Vercel's and stays where it is no matter which framework renders the page.

Stacks where the tags reach the CDN untouched avoid the mapping layer. Astro is a good example on the framework side: its Cache API takes context.cache.set({ maxAge, tags }) and cache.invalidate({ tags }), and the adapter's cache provider forwards the tags to the platform's own header and purge API, with nothing in between. You end up with the same shape as the plain-HTTP example above, minus the platform details. Providers exist for Cloudflare, Netlify and Vercel alike, though on Vercel you keep the simplicity and still inherit the 128-tag ceiling.

The hosting platform decides most of this, not the framework. Cloudflare, Netlify and Fastly all carry more tags than we emit and expose a purge-by-tag API that any code can call, so on those platforms any framework able to set a response header can integrate cache tags. That is the first half of this page, applied to a managed platform instead of your own CDN. A framework adds ergonomics on top: with Astro you never touch a header or a purge endpoint, because its providers wrap those same platform APIs for you.

If you are still choosing, we would point you toward a platform that keeps cache tags verbatim from end to end and leaves room for all 500 of them, so Cloudflare, Netlify or Fastly, and then pick whichever framework you would have picked anyway. Both costs described above are worth avoiding when you can: a mapping layer is infrastructure you operate, and a tag ceiling below 500 is a limit you will hit one day without anyone telling you.

Our guides

Each of these walks through one stack end to end, from the first query to the invalidation webhook:

If yours isn't here, you are not stuck: the three questions above will tell you most of what to expect from it, and the plain-HTTP integration at the top of this page works with any server that can set a response header, behind any CDN that can purge by tag.

Last updated: