Back to Denoland

CDN and caching

deploy/reference/caching.md

latest11.4 KB
Original Source

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

When a request arrives at Deno Deploy:

  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 cache follows RFC 9110 and RFC 9111 semantics, implementing standard HTTP caching behavior.

Cache-Control Headers

Deno Deploy respects the standard Cache-Control header with the following directives:

Response Directives

DirectiveDescription
publicResponse can be cached by shared caches
privateResponse is user-specific and cannot be cached (bypasses CDN)
no-storeResponse must not be cached
no-cacheResponse must be revalidated before use
max-age=NResponse is fresh for N seconds
s-maxage=NLike max-age, but only for shared caches (takes precedence)
stale-while-revalidate=NServe stale content while revalidating in background
stale-if-error=NServe stale content if origin returns an error
must-revalidateStale responses must not be used without revalidation

Example: Cache for 1 hour at the edge

typescript
Deno.serve(() => {
  return new Response("Hello, World!", {
    headers: {
      "Cache-Control": "public, s-maxage=3600",
    },
  });
});

Example: Cache with stale-while-revalidate

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",
    },
  });
});

Deno Deploy-Specific Headers

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:

ValueDescription
deno; hitResponse served from cache
deno; fwd=uri-miss; storedCache miss, response stored for future requests
deno; fwd=miss; storedVary miss, response stored with new vary key
deno; fwd=staleStale response, forwarding to origin
deno; fwd=methodNon-cacheable method (POST, PUT, etc.)
deno; fwd=bypassResponse explicitly bypassed cache
deno; fwd=requestRequest 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.