On this page
CDN and caching
Deno Deploy includes a built-in HTTP caching layer that automatically caches eligible responses at the edge, reducing latency and origin load. This document covers how caching works, how to control cache behavior, and how to invalidate cached content.
How Caching Works Jump to heading
When a request arrives at Deno Deploy:
- The cache checks if a valid cached response exists for the request
- If found and fresh, the cached response is served immediately (cache hit)
- If not found or stale, the request is forwarded to your application (cache miss)
- Cacheable responses are stored for future requests
The cache follows RFC 9110 and RFC 9111 semantics, implementing standard HTTP caching behavior.
Cache-Control Headers Jump to heading
Deno Deploy respects the standard Cache-Control header with the following
directives:
Response Directives Jump to heading
| Directive | Description |
|---|---|
public |
Response can be cached by shared caches |
private |
Response is user-specific and cannot be cached (bypasses CDN) |
no-store |
Response must not be cached |
no-cache |
Response must be revalidated before use |
max-age=N |
Response is fresh for N seconds |
s-maxage=N |
Like max-age, but only for shared caches (takes precedence) |
stale-while-revalidate=N |
Serve stale content while revalidating in background |
stale-if-error=N |
Serve stale content if origin returns an error |
must-revalidate |
Stale responses must not be used without revalidation |
Example: Cache for 1 hour at the edge Jump to heading
Deno.serve(() => {
return new Response("Hello, World!", {
headers: {
"Cache-Control": "public, s-maxage=3600",
},
});
});
Example: Cache with stale-while-revalidate Jump to heading
Deno.serve(() => {
return new Response(JSON.stringify({ data: "..." }), {
headers: {
"Content-Type": "application/json",
// Cache for 60s, serve stale for up to 5 minutes while revalidating
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
},
});
});
Deno Deploy-Specific Headers Jump to heading
Deno Deploy supports additional headers for fine-grained cache control:
Deno-CDN-Cache-Control Jump to heading
A CDN-specific cache control header that takes precedence over both
CDN-Cache-Control and Cache-Control. Use this when you want different
caching behavior for Deno Deploy's CDN versus browsers or other caches.
Header priority (highest to lowest):
Deno-CDN-Cache-ControlCDN-Cache-ControlCache-Control
Deno.serve(() => {
return new Response("Hello!", {
headers: {
// Browser caches for 60s
"Cache-Control": "public, max-age=60",
// Deno Deploy CDN caches for 1 hour
"Deno-CDN-Cache-Control": "public, s-maxage=3600",
},
});
});
Deno-Cache-Tag / Cache-Tag Jump to heading
Associate responses with cache tags for targeted invalidation. Tags allow you to invalidate groups of cached responses without knowing their exact URLs.
Deno.serve((req) => {
const url = new URL(req.url);
const productId = url.pathname.split("/")[2];
return new Response(JSON.stringify({ id: productId, name: "Widget" }), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, s-maxage=3600",
// Tag this response for later invalidation
"Deno-Cache-Tag": `product-${productId},products,catalog`,
},
});
});
Tag format:
- Multiple tags can be specified as a comma-separated list
- Tags are case-insensitive
- Maximum 1024 characters per tag
- Maximum 500 tags per response
- Tags must be UTF-8 encoded
Deno-Cache-Id Jump to heading
A special header that serves two purposes:
-
Opt out of automatic invalidation: Responses with
Deno-Cache-Iduse a shared cache namespace that persists across deployments. Without this header, cached responses are automatically invalidated when you deploy a new version. -
Acts as a cache tag: The value can be used to invalidate the cached response.
Deno.serve(() => {
return new Response("Static content that rarely changes", {
headers: {
"Cache-Control": "public, s-maxage=86400",
// This response survives redeployments
"Deno-Cache-Id": "static-content-v1",
},
});
});
Use cases for Deno-Cache-Id:
- Content that should remain cached across deployments (e.g., static assets with content-based hashes)
- Long-lived cached responses where you want explicit control over invalidation
- Sharing cached responses between deployment revisions
Deno-Vary Jump to heading
Extend cache variation beyond standard HTTP Vary semantics. (Coming soon)
Cache Invalidation Jump to heading
Deno Deploy supports on-demand cache invalidation via cache tags. This allows you to purge specific cached content without redeploying.
Invalidation API Jump to heading
Send a POST request to http://cache.localhost/invalidate/http from within your
Deno Deploy application:
Deno.serve(async (req) => {
const url = new URL(req.url);
if (req.method === "POST" && url.pathname === "/admin/purge") {
// Invalidate all responses tagged with "products"
const res = await fetch("http://cache.localhost/invalidate/http", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tags: ["products"],
}),
});
if (res.ok) {
return new Response("Cache purged successfully");
}
return new Response("Purge failed", { status: 500 });
}
// ... handle other requests
});
Invalidation Request Format Jump to heading
{
"tags": ["tag1", "tag2", "tag3"]
}
tags: Array of cache tags to invalidate (required)- Maximum 500 tags per request
Wildcard Invalidation Jump to heading
Use "*" to invalidate all cached responses for your deployment:
await fetch("http://cache.localhost/invalidate/http", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tags: ["*"],
}),
});
Cross-Region Invalidation Jump to heading
Cache invalidation is automatically synchronized across all Deno Deploy regions. When you invalidate a tag, the purge propagates globally within seconds.
Example: Content Management Webhook Jump to heading
Deno.serve(async (req) => {
const url = new URL(req.url);
// Webhook endpoint for CMS updates
if (req.method === "POST" && url.pathname === "/webhook/cms") {
const payload = await req.json();
// Invalidate based on content type
const tags: string[] = [];
if (payload.type === "product") {
tags.push(`product-${payload.id}`, "products");
} else if (payload.type === "category") {
tags.push(`category-${payload.id}`, "categories", "navigation");
}
if (tags.length > 0) {
await fetch("http://cache.localhost/invalidate/http", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tags }),
});
}
return new Response("OK");
}
// ... serve content
});
Cache Status Header Jump to heading
Deno Deploy adds a Cache-Status response header to indicate the cache result:
| Value | Description |
|---|---|
deno; hit |
Response served from cache |
deno; fwd=uri-miss; stored |
Cache miss, response stored for future requests |
deno; fwd=miss; stored |
Vary miss, response stored with new vary key |
deno; fwd=stale |
Stale response, forwarding to origin |
deno; fwd=method |
Non-cacheable method (POST, PUT, etc.) |
deno; fwd=bypass |
Response explicitly bypassed cache |
deno; fwd=request |
Request directives prevented caching |
Bypass Details Jump to heading
When a response bypasses the cache, the detail field indicates why:
detail=not-cacheable- Response doesn't meet caching criteriadetail=no-cache-or-private-no-cacheorprivatedirective presentdetail=set-cookie- Response containsSet-Cookieheaderdetail=pragma-no-cache- LegacyPragma: no-cacheheader presentdetail=too-large- Response body exceeds maximum cacheable sizedetail=zero-ttl- Calculated TTL is zerodetail=vary-star-Vary: *header prevents cachingdetail=header-overflow- Too many response headers
Cacheable Responses Jump to heading
A response is cacheable when:
- The request method is
GETorHEAD - The response status is cacheable (200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501)
- The response includes caching headers (
Cache-Control,Expires, etc.) - The response doesn't include
no-store,private, orno-cachedirectives - The response doesn't include a
Set-Cookieheader - The response body is within the maximum cacheable size
Cache Variation (Vary) Jump to heading
The cache respects the standard Vary header to store different versions of a
response based on request headers:
Deno.serve((req) => {
const acceptLanguage = req.headers.get("Accept-Language") || "en";
const language = acceptLanguage.startsWith("es") ? "es" : "en";
return new Response(`Hello in ${language}!`, {
headers: {
"Cache-Control": "public, s-maxage=3600",
"Vary": "Accept-Language",
"Content-Language": language,
},
});
});
Note: Vary: * prevents caching entirely.
HEAD Requests Jump to heading
HEAD requests can be served from cached GET responses. The cache automatically strips the response body while preserving headers.
Range Requests Jump to heading
The cache supports HTTP range requests (Range header) and can serve partial
content from cached full responses.
Best Practices Jump to heading
1. Use s-maxage for CDN caching Jump to heading
// Let browsers cache for 1 minute, CDN for 1 hour
headers: {
"Cache-Control": "public, max-age=60, s-maxage=3600"
}
2. Tag content for targeted invalidation Jump to heading
// Tag by content type, ID, and category for flexible purging
headers: {
"Deno-Cache-Tag": `article-${id},articles,category-${category}`
}
3. Use stale-while-revalidate for better UX Jump to heading
// Serve stale content while refreshing in background
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=600"
}
4. Use Deno-Cache-Id for stable assets Jump to heading
// Content-addressed assets that shouldn't be invalidated on deploy
const hash = computeHash(content);
headers: {
"Cache-Control": "public, s-maxage=31536000, immutable",
"Deno-Cache-Id": `asset-${hash}`
}
5. Set appropriate TTLs Jump to heading
- Static assets with hash in filename:
max-age=31536000(1 year) - API responses:
s-maxage=60tos-maxage=300withstale-while-revalidate - Personalized content: Don't cache or use
Varyappropriately
Automatic Cache Invalidation Jump to heading
By default, all cached responses (without Deno-Cache-Id) are automatically
invalidated when you deploy a new version of your application. This ensures
users always see fresh content after a deployment.
To opt out of automatic invalidation, use the Deno-Cache-Id header.