Skip to main content
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:

  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 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):

  1. Deno-CDN-Cache-Control
  2. CDN-Cache-Control
  3. Cache-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:

  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.

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 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 Jump to heading

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) 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=60 to s-maxage=300 with stale-while-revalidate
  • Personalized content: Don't cache or use Vary appropriately

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.

Did you find what you needed?

Privacy policy