Advanced caching made easy

by Matt Kane

Netlify’s global network supports a powerful range of cache features, most of which can be controlled via response headers. Now you don’t need to remember the different header names, directives and best practices to serve dynamic content that is fast and fresh.

#TL;DR

The cdn-cache-control package has a simple API and sensible defaults that makes it easy to generate the headers needed for common caching tasks. It works anywhere that lets you set response headers, including Netlify Functions and Edge Functions, as well as natively within most web frameworks.

#Advanced caching on Netlify

Netlify supports several advanced cache directives that can be controlled via response headers. This is particularly useful for server-side rendering of web content, as it allows you to manually handle the invalidation of content, ensuring it stays fast and fresh. You can either set the cached responses to expire after a certain period, or purge it manually using the API. Using stale-while-revalidate allows you to update content in the background without blocking requests.

#Manually setting headers

The full caching docs show how to have full control over your cache headers. You can use standard Cache-Control and CDN-Cache-Control headers, or target the cache more specifically with the Netlify-CDN-Cache-Control header. You can also use the Netlify-Cache-Tag header to invalidate cached content. On-demand invalidation allows you to purge content from the cache individually or in bulk. Because this is based on standard HTTP headers, it works with any web framework or function. You can see detailed guides for Remix and Astro in the Netlify developer hub.

#Using the cdn-cache-control package

While manually setting headers gives you powerful, fine-grained control over the cache it can be hard to remember the correct headers to set, and the best practices for different types of content. I created the cdn-cache-control package to make it easy to generate the correct headers for common use cases. It extends the Fetch standard Headers class, adding a simple, chainable API with sensible defaults for common use cases. It works by setting the Cache-Control and Netlify-CDN-Cache-Control headers to the appropriate values. It also has helpers for setting the Netlify-Cache-Tag header, which is used to invalidate cached content.

#Installing the package

The package can be installed from npm:

Terminal window
npm install cdn-cache-control

#Using the package

The module exports a single class, CacheHeaders, which is a subclass of the fetch Headers class. By default it sets the Cache-Control and Netlify-CDN-Cache-Control headers to sensible values for content that should be cached by the CDN and revalidated by the browser. It also provides a chainable API for setting cache headers.

Like a regular Headers object it can optionally be created with a value to pre-populate the header values. This can be an existing Headers object, a plain object or array with existing header values. In that case it will default to using existing s-maxage directives if present.

This example shows simple usage of the package:

import { CacheHeaders } from "cdn-cache-control";
import type { Config } from "@netlify/functions";
export default async function handler(request: Request): Promise<Response> {
const headers = new CacheHeaders();
return new Response("Hello, world!", { headers });
}
export const config: Config {
path: "/hello"
}

This sets the Netlify-CDN-Cache-Control header to public,s-maxage=31536000,durable,must-revalidate, which tells the CDN to cache the content for a year and use Netlify’s Durable Cache for improved performance. It sets Cache-Control to public,max-age=0,must-revalidate, which tells the browser to always check with the CDN for a fresh version. You should combine this with an ETag or Last-Modified header to allow the CDN to serve a 304 Not Modified response when the content hasn’t changed.

#Tagging content for invalidation

If you need to purge cached content you can add cache tags to your responses. This is particularly useful if your responses are based on an API that may change. You can use a webhook to purge the cache when content changes. For a complete example see the Astro guide, but read on for a simple guide.

In this example we add a cache tag to the response based on the id of the data:

import type { Config, Context } from "@netlify/functions";
import { CacheHeaders } from "cdn-cache-control";
import etag from "etag";
import { getPet } from "./pets-api.js";
export default async function handler(request: Request, context: Context): Promise<Response> {
const pet = await getPet(context.params.slug);
if(!pet) {
return new Response("Not found", { status: 404 });
}
// Create the cache headers, setting the ETag to the hash of the pet object
const headers = new CacheHeaders({
"etag": etag(JSON.stringify(pet)),
// Use the chained 'tag' method to add a cache tag
}).tag("pets", "pet-" + pet.id);
return Response.json(pet, { headers });
}
export const config: Config {
path: "/pets/:slug"
}

#Expiring content

By default the CacheHeaders class will cache your content for up to a year, or until you next deploy. If you want to set a different expiry time you can use the ttl (time to live) method. You can pass a number of seconds to the ttl method to set a different value. There are helper constants for common values:

import { CacheHeaders, ONE_WEEK } from "cdn-cache-control";
// Cache content for up to a week
const headers = new CacheHeaders().ttl(ONE_WEEK);

#Faster loading with stale-while-revalidate

Enabling the stale-while-revalidate directive tells the CDN to return stale content after it has expired, but then regenerate the content in the background. This is good for pages that may be slow to render, but where it’s ok to return stale content for a while. You can enable stale-while-revalidate with the swr method:

import { CacheHeaders } from "cdn-cache-control";
const headers = new CacheHeaders().swr();

By default it will return stale content for up to a week, but you can pass a number of seconds to the swr method to set a different value.

import { CacheHeaders, ONE_DAY } from "cdn-cache-control";
// Return stale content for up to an hour, and revalidate it in the background
const headers = new CacheHeaders().swr(ONE_HOUR);

By default, calling swr will expire content immediately and regenerate it after each request. If you’re happy to keep the content fresh for a while you can use the ttl method alongside swr.

import { CacheHeaders, ONE_HOUR } from "cdn-cache-control";
// Content is fresh for 30 seconds, then return stale content for up to an hour
const headers = new CacheHeaders().ttl(30).swr(ONE_HOUR);

#Immutable content

Sometimes you are generating content that you know will never change. This might be an asset that includes a hash in the URL, or other unchanging unique content. For these you can use the immutable to cache the content for the year in both the CDN and the browser, adding the immutable directive to the headers.

import { CacheHeaders } from "cdn-cache-control";
// Cache content for up to a year, and tell the browser it will never change
const headers = new CacheHeaders().immutable();

Careful

Be careful when using immutable as it will cache the content for a year in the browser. If you need to change the content you will need to change the URL or the content itself. It is best to restrict its use to cases where the URL is guaranteed to be unique and unchanging, such as assets with a hash in the URL.

#Using the CacheHeaders object with a framework

The CacheHeaders object works perfectly with frameworks SSR functions. It can be used anywhere you can set response headers. It is a valid fetch Headers object, so can be used with the Response constructor. Some frameworks use a readonly Headers object, where you need to set the headers in the response object directly. In this case you can use the copyTo method to copy the headers to the response object:

---
import { CacheHeaders, ONE_HOUR } from "cdn-cache-control";
new CacheHeaders().swr(ONE_HOUR).copyTo(Astro.response.headers);
---

#Troubleshooting

If you’re having trouble, the Cache-Status header can be useful for debugging. It shows the status of the cache for the request, including whether the content was served from the cache or not, and whether it was stale or fresh. For more details see the cache debugging docs.

#Learn more

For more details and full API docs, see cdn-cache-control on GitHub.