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:
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:
Vercel-CDN-Cache-ControlCDN-Cache-ControlCache-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-ControlandVercel-CDN-Cache-Controlare removed from the response.- A
Cache-Controlthat containss-maxageis rewritten topublic, max-age=0, must-revalidate. - A response with no
Cache-Controlgetspublic, 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:
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:
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:
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
GETorHEAD. - The status is
200,301,302,307,308, or404. - The selected policy has a positive shared-cache lifetime.
- The response isn't marked
private,no-store, orno-cache. - The response has no
Set-Cookieheader. - The request has no
Authorizationheader. - 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:
| Value | Meaning |
|---|---|
HIT | A fresh cached response was served |
STALE | A stale response was served inside a stale-while-revalidate window while Easel refreshes it |
MISS | No usable entry was found; the response came from your code or the deployment |
BYPASS | The request or response isn't eligible for caching |
A cacheable route returns MISS on the first request and HIT on the next:
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
- Revalidation: refresh content before it expires.
- Cache observability: hit rates per route.
- CDN troubleshooting: fix
MISSandBYPASS.