From 3688ab325b47b70069dc3578606b7f55b7a4e1be Mon Sep 17 00:00:00 2001 From: qti3e Date: Fri, 13 Feb 2026 14:29:00 -0800 Subject: [PATCH 1/2] update cache docs --- deploy/reference/caching.md | 409 ++++++++++++++++++++++++++++++++---- 1 file changed, 366 insertions(+), 43 deletions(-) diff --git a/deploy/reference/caching.md b/deploy/reference/caching.md index 402b90e9c..eee823207 100644 --- a/deploy/reference/caching.md +++ b/deploy/reference/caching.md @@ -1,60 +1,383 @@ --- -title: Caching -description: "Overview of CDN caching functionality in Deno Deploy, including cache configuration, directives, and best practices." +title: CDN and caching +description: "HTTP caching in Deno Deploy: cache control headers, cache tags, invalidation API, and best practices for edge caching." --- -Deno Deploy includes a built-in CDN that can cache responses from your -application. This improves performance for: +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. -- Static assets (images, CSS, JavaScript files) -- API responses and server-rendered pages that don't change frequently +## How Caching Works -Caching is enabled by default for all applications, but only responses with -appropriate caching headers are actually cached. +When a request arrives at Deno Deploy: -Deno Deploy integrates with popular frameworks like Next.js to automatically -optimize caching for features such as Incremental Static Regeneration (ISR). +1. The cache checks if a valid cached response exists for the request +2. If found and fresh, the cached response is served immediately (cache hit) +3. If not found or stale, the request is forwarded to your application (cache + miss) +4. Cacheable responses are stored for future requests -The CDN cache is tied to both the revision and context. When you deploy a new -revision, the cache is automatically invalidated, ensuring users always see the -latest version of your application. Note that browser caching may still serve -older content if the `Cache-Control` header permits it. +The cache follows [RFC 9110](https://httpwg.org/specs/rfc9110.html) and +[RFC 9111](https://httpwg.org/specs/rfc9111.html) semantics, implementing +standard HTTP caching behavior. -## Caching a resource +## Cache-Control Headers -To cache a resource, set the `Cache-Control` header in your response. This -standard HTTP header tells browsers and the CDN how to cache your content. +Deno Deploy respects the standard `Cache-Control` header with the following +directives: -### Supported caching directives +### Response Directives -Deno Deploy supports these caching directives: +| 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 | -| Directive | Description | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `max-age` | Maximum time (in seconds) the response is considered fresh by both CDN and browsers. After this time, the response is considered stale and revalidated with the server. | -| `s-maxage` | Maximum time (in seconds) the response is considered fresh by shared caches (CDNs only, not browsers). After this time, the response is revalidated with the server. | -| `stale-while-revalidate` | Maximum time (in seconds) a stale response can be served while a fresh one is fetched in the background. | -| `stale-if-error` | Maximum time (in seconds) a stale response can be served if the server returns an error. | -| `immutable` | Indicates the response will never change, allowing indefinite caching. Ideal for content-hashed static assets. | -| `no-store` | Prevents caching of the response. Use for dynamic content that should never be cached. | -| `no-cache` | Requires revalidation with the server before serving from cache. Use for content that changes frequently but can benefit from conditional requests. | +### Example: Cache for 1 hour at the edge -### Additional caching headers +```typescript +Deno.serve(() => { + return new Response("Hello, World!", { + headers: { + "Cache-Control": "public, s-maxage=3600", + }, + }); +}); +``` -- `Vary`: Specifies which request headers should be included in the cache key, - creating separate cached versions based on those headers. +### Example: Cache with stale-while-revalidate -- `Expires`: Sets an absolute expiration date for the response (alternative to - `max-age`). do not change, such as images or CSS files. -- `no-store`: The response should not be cached. This is useful for dynamic - responses that should not be cached, such as API responses or server rendered - pages. -- `no-cache`: The response should be revalidated with the server before being - served from the cache. This is useful for dynamic responses that may change - frequently. +```typescript +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", + }, + }); +}); +``` -The `Vary` header can be used to specify which request headers should be part of -the cache key for the request. +## Deno Deploy-Specific Headers -The `Expires` header can be used to specify an absolute expiration date for the -response. This is an alternative to the `max-age` directive. +Deno Deploy supports additional headers for fine-grained cache control: + +### `Deno-CDN-Cache-Control` + +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):** + +1. `Deno-CDN-Cache-Control` +2. `CDN-Cache-Control` +3. `Cache-Control` + +```typescript +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` + +Associate responses with cache tags for targeted invalidation. Tags allow you to +invalidate groups of cached responses without knowing their exact URLs. + +```typescript +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` + +A special header that serves two purposes: + +1. **Opt out of automatic invalidation**: Responses with `Deno-Cache-Id` use a + shared cache namespace that persists across deployments. Without this header, + cached responses are automatically invalidated when you deploy a new version. + +2. **Acts as a cache tag**: The value can be used to invalidate the cached + response. + +```typescript +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` + +Extend cache variation beyond standard HTTP `Vary` semantics. _(Coming soon)_ + +## Cache Invalidation + +Deno Deploy supports on-demand cache invalidation via cache tags. This allows +you to purge specific cached content without redeploying. + +### Invalidation API + +Send a POST request to `http://cache.localhost/invalidate/http` from within your +Deno Deploy application: + +```typescript +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 + +```json +{ + "tags": ["tag1", "tag2", "tag3"] +} +``` + +- `tags`: Array of cache tags to invalidate (required) +- Maximum 500 tags per request + +### Wildcard Invalidation + +Use `"*"` to invalidate all cached responses for your deployment: + +```typescript +await fetch("http://cache.localhost/invalidate/http", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + tags: ["*"], + }), +}); +``` + +### Cross-Region Invalidation + +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 + +```typescript +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 + +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 + +When a response bypasses the cache, the `detail` field indicates why: + +- `detail=not-cacheable` - Response doesn't meet caching criteria +- `detail=no-cache-or-private` - `no-cache` or `private` directive present +- `detail=set-cookie` - Response contains `Set-Cookie` header +- `detail=pragma-no-cache` - Legacy `Pragma: no-cache` header present +- `detail=too-large` - Response body exceeds maximum cacheable size +- `detail=zero-ttl` - Calculated TTL is zero +- `detail=vary-star` - `Vary: *` header prevents caching +- `detail=header-overflow` - Too many response headers + +## Cacheable Responses + +A response is cacheable when: + +1. The request method is `GET` or `HEAD` +2. The response status is cacheable (200, 203, 204, 206, 300, 301, 308, 404, + 405, 410, 414, 501) +3. The response includes caching headers (`Cache-Control`, `Expires`, etc.) +4. The response doesn't include `no-store`, `private`, or `no-cache` directives +5. The response doesn't include a `Set-Cookie` header +6. The response body is within the maximum cacheable size + +## Cache Variation (Vary) + +The cache respects the standard `Vary` header to store different versions of a +response based on request headers: + +```typescript +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 + +HEAD requests can be served from cached GET responses. The cache automatically +strips the response body while preserving headers. + +## Range Requests + +The cache supports HTTP range requests (`Range` header) and can serve partial +content from cached full responses. + +## Best Practices + +### 1. Use `s-maxage` for CDN caching + +```typescript +// 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 + +```typescript +// 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 + +```typescript +// 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 + +```typescript +// 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 + +- Static assets with hash in filename: `max-age=31536000` (1 year) +- API responses: `s-maxage=60` to `s-maxage=300` with `stale-while-revalidate` +- Personalized content: Don't cache or use `Vary` appropriately + +## Automatic Cache Invalidation + +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. From 283ec3e418c43f2d6cd2546f957e7333025818e1 Mon Sep 17 00:00:00 2001 From: qti3e Date: Fri, 13 Feb 2026 14:32:03 -0800 Subject: [PATCH 2/2] fmt --- deploy/reference/caching.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/deploy/reference/caching.md b/deploy/reference/caching.md index eee823207..5c42546b3 100644 --- a/deploy/reference/caching.md +++ b/deploy/reference/caching.md @@ -29,17 +29,17 @@ directives: ### Response Directives -| 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 | +| 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 @@ -150,8 +150,8 @@ Deno.serve(() => { **Use cases for `Deno-Cache-Id`:** -- Content that should remain cached across deployments (e.g., static assets - with content-based hashes) +- 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