CDN, caching and what the platform owns
CDN, caching and what the platform owns in the Deploy guide.
Built assets on a CDN. dusk({ assetOrigin: "https://cdn.example" }) makes the built bundles, the CSS and the
files of the assets folder refer to <origin><base><file> (for base: "/", https://cdn.example/assets/app-….js). The
CDN must serve those files at that path: upload the assets/ folder of dist/client to it. Routes, public/ files
and the server stay on the app's own origin, and base still sets the route mount prefix. Without the option, nothing
changes. The option is validated: an origin only, with no path, query or credentials.
Immutable files. Every adapter sends Cache-Control: public, max-age=31536000, immutable for the build's assets
folder and for fonts/google/ (the Google fonts Dusk vendors, named by their content hash): the _headers files of
Netlify and Cloudflare, the routes of Vercel's config.json, and the Node static server's default. The Vite manifest
(.vite/manifest.json) is read by the build and then removed, so no platform output carries it.
Pages on the CDN. An ISR page (revalidate) sends Cache-Control: public, max-age=0, s-maxage=<lifetime>, stale-while-revalidate=<stale window> and an Age (seconds since its render began). A shared cache subtracts Age from s-maxage, so it keeps the page for what is left of its fresh window and expires it when the server does; a CDN that ignores Age keeps it up to one lifetime longer than the server. A page with revalidate: false
sends s-maxage=31536000 instead of must-revalidate alone, so a CDN keeps it until a purge or a new build. A dynamic
page (no revalidate) is not cached by Dusk: the handler answers private, no-store. A route rule can ask for the
CDN's own copy of the same policy: routeRules: { "/news": { revalidate: 60, cache: { cdn: true } } } sends
CDN-Cache-Control with the same directives, and cache: { cdn: { surrogate: true } } also sends Surrogate-Control.
A page keyed on cookies is private and sends neither.
Vary. A page that varies on a request header (a locale negotiated from Accept-Language) is cached once per value of
that header: the Vary names are stored with the page, and each value gets its own entry, so two languages on one path
never share a body. A page that varies on * is answered but not stored. A request with a cookie the page does not
declare bypasses the cache. The i18n middleware's Vary: Accept-Language is read the same way. Every compressible
answer (HTML, JSON, text, SVG) carries Vary: Accept-Encoding, whether or not Dusk compresses it.
Purge is in-process. revalidatePath and revalidateTag remove entries from the page cache of the process that
calls them, or from the shared store when cache.kv or cache.store (for example redisStore) is set. They do not send a purge to a CDN. After a deploy, purge the
CDN with its own API, or let the CDN's short max-age=0 revalidation catch up.
Compression on serverless. Dusk compresses on Node (buffered and streamed text). Bun and Deno compress buffered text with a known length only. On Vercel, Netlify and Cloudflare the platform compresses: Dusk does not, and the adapter test suite does not measure it.
Early Hints. The Node server sends 103 Early Hints for page requests (see Early Hints). On the other
platforms the Link preload headers come with the HTML a handler renders; a platform that supports Early Hints can turn them
into a 103 in front.
Service workers. Dusk does not set Service-Worker-Allowed. A service worker's scope is the path of its file
(/sw.js controls /). A wider or narrower scope is a header the app sets on that file, through the route rules'
headers.
Image optimizer and DNS. /_dusk/image refuses, even when the origin is allowed, a literal address in a loopback,
private, link-local, CGNAT or unique-local range, and localhost, *.localhost, *.local and *.internal names. It
resolves a remote name and refuses it when any answer is such an address, at the connect step: on Node that is
safeImageFetch, the default there (see the security guide). On Bun, Deno and Workers the platform's fetch is used,
and the framework does not check the answers of a name: refuse private answers at the platform or the egress firewall.
Its default qualities is [75]; list the others in images.qualities to serve them.