Skip to main content
CDN

Caching

How Easel caches static assets and dynamic HTTP responses at the CDN.

Easel caches static files and dynamic HTTP responses so repeated requests are served without running your code again. Static deployment assets are handled automatically. Dynamic responses are cached when they include a shared-cache policy with a positive lifetime.

Cache a dynamic response

Return a shared-cache directive with a positive lifetime:

Cache-Control: public, max-age=0, must-revalidate
CDN-Cache-Control: public, s-maxage=3600

Browsers revalidate on every visit, while Easel reuses the response for an hour without invoking your function.

Cache-Control headers

Easel reads these response headers, highest precedence first, and uses only the first one present:

  1. Vercel-CDN-Cache-Control
  2. CDN-Cache-Control
  3. Cache-Control

Use CDN-Cache-Control when the browser and the CDN need different policies. Vercel-CDN-Cache-Control is honored so apps migrated from Vercel keep their behavior.

Easel consumes the CDN policy and doesn't pass it to browsers:

  • CDN-Cache-Control and Vercel-CDN-Cache-Control are removed from the response.
  • A Cache-Control that contains s-maxage is rewritten to public, max-age=0, must-revalidate.
  • A response with no Cache-Control gets public, max-age=0, must-revalidate.

A Cache-Control without s-maxage, such as a long max-age on a hashed asset, reaches the browser unchanged. Because the CDN headers are removed, curl shows the browser policy, not the one Easel cached with. Check X-Easel-Cache to see what the CDN did.

A higher-precedence header wins even when it's stricter. This response isn't cached:

Vercel-CDN-Cache-Control: no-store
CDN-Cache-Control: public, s-maxage=3600

Lifetime

s-maxage sets how long Easel treats a response as fresh. Without it, Easel uses max-age from the selected header. A policy with no lifetime, such as Cache-Control: public alone, isn't cached.

Browsers follow Cache-Control independently:

Cache-Control: public, max-age=60
CDN-Cache-Control: public, s-maxage=3600

Here the browser reuses its copy for a minute, and Easel serves its copy for an hour. Purging the CDN can't clear a browser's copy, so keep the browser max-age short for content you revalidate, or put s-maxage in Cache-Control and let Easel rewrite it. See Revalidation.

Stale-while-revalidate

When the fresh lifetime ends, the next request waits while Easel fetches a new response. To serve the old response while refreshing in the background, set an explicit window:

CDN-Cache-Control: public, s-maxage=60, stale-while-revalidate=300

For 60 seconds the response is fresh (HIT). For the next 300 seconds Easel serves the stale body (STALE) and refreshes it in the background. After that, the next request waits for a fresh response. Easel doesn't add a stale window when the directive is absent, though frameworks such as Next.js may emit one for you.

Cache eligibility

A dynamic response is cached when all of these are true:

  • The request method is GET or HEAD.
  • The status is 200, 301, 302, 307, 308, or 404.
  • The selected policy has a positive shared-cache lifetime.
  • The response isn't marked private, no-store, or no-cache.
  • The response has no Set-Cookie header.
  • The request has no Authorization header.
  • The request isn't a range request.

Otherwise Easel returns X-Easel-Cache: BYPASS. Responses above the cacheable size are served but not stored; see Limits.

Mark user-specific responses Cache-Control: private, no-store. To cache a page that also sets a cookie, set the cookie on a separate endpoint and redirect to the cacheable page.

Cache results

X-Easel-Cache is the authoritative cache result on every response:

ValueMeaning
HITA fresh cached response was served
STALEA stale response was served inside a stale-while-revalidate window while Easel refreshes it
MISSNo usable entry was found; the response came from your code or the deployment
BYPASSThe request or response isn't eligible for caching

A cacheable route returns MISS on the first request and HIT on the next:

curl -I https://example.com/products
curl -I https://example.com/products

An Age header appears on some cached responses. Its absence doesn't mean the response wasn't cached.

Cache keys

Cache entries are scoped to the deployment and keyed by path and query string. A new deployment starts with an empty cache. /products?page=1 and /products?page=2 are separate entries, so drop tracking and other unneeded query parameters from cacheable URLs. Static files from your build are the exception: they're keyed by the file served, so a query string such as ?v=2 doesn't create another copy.

Each entry is stored once per encoding: Brotli, gzip, and uncompressed. The client's Accept-Encoding picks the copy, and a hit is sent as stored without compressing it again. Purging a path clears every encoding.

Other Vary values don't split entries: every request for the same path and query string gets the same cached response. If a response depends on a request header, such as Accept-Language or a cookie, put the variation in the URL (for example /en/products) or don't cache it.

Static deployment assets

Static files are versioned with the deployment and served without a function. Content-hashed files such as /assets/app.9f3ac21.js can use long lifetimes because a changed file gets a new URL. Files with stable URLs, such as HTML or robots.txt, need shorter policies. See Static files.

Framework caching

Supported frameworks generate cache policies and tags for you, such as Next.js ISR and Cache Components or Nuxt route rules. Easel maps them onto the CDN and the Runtime Cache. Check X-Easel-Cache to see the result. See the framework guides.

Next steps