Edge Caching
Edge caching strategies for optimizing content delivery and reducing origin load
You are an expert in edge caching strategies for building edge-first applications that minimize latency and origin load through intelligent cache management. ## Key Points 1. **Browser cache** — controlled by `Cache-Control` and `ETag` headers 2. **CDN edge cache** — the CDN's PoP stores responses based on cache headers or API directives 3. **Application cache** — in-memory or KV-based caching within edge functions 4. **Origin cache** — Redis, Varnish, or similar caches at the origin server - **Use `s-maxage` to separate CDN and browser cache durations** — browsers often need shorter TTLs than the CDN edge. - **Hash static asset filenames** — this enables infinite `max-age` with `immutable`, since new content gets a new URL. - **Implement `stale-while-revalidate`** — it provides the best user experience by serving cached content instantly while refreshing in the background. - **Normalize cache keys** — sort query parameters, strip tracking parameters, and lowercase paths to avoid duplicate cache entries. - **Use `Vary` sparingly** — each unique combination of `Vary` header values creates a separate cache entry. Over-varying fragments the cache. - **Set `Cache-Control: private` on authenticated responses** — prevents CDN from caching user-specific content and serving it to other users. - **Monitor cache hit ratios** — use CDN analytics to identify low-hit-ratio paths and adjust TTLs or caching strategies accordingly. - **Caching responses with `Set-Cookie`** — CDNs may strip cookies from cached responses or, worse, serve one user's session cookie to another. Never cache responses that set cookies.
skilldb get edge-computing-skills/Edge CachingFull skill: 358 linesEdge Caching Strategies — Edge Computing
You are an expert in edge caching strategies for building edge-first applications that minimize latency and origin load through intelligent cache management.
Overview
Edge caching stores content at CDN edge nodes close to end users, reducing round trips to origin servers. Effective caching strategies combine HTTP cache headers, CDN-specific APIs, and application-level cache logic to serve content in single-digit milliseconds. The key challenge is balancing freshness against performance — serving stale content is fast but potentially incorrect, while revalidating every request eliminates the benefit of caching.
Key caching layers:
- Browser cache — controlled by
Cache-ControlandETagheaders - CDN edge cache — the CDN's PoP stores responses based on cache headers or API directives
- Application cache — in-memory or KV-based caching within edge functions
- Origin cache — Redis, Varnish, or similar caches at the origin server
Core Concepts
Cache-Control Headers
// Immutable static assets (hashed filenames)
new Response(body, {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
},
});
// Dynamic content that can be cached briefly
new Response(body, {
headers: {
"Cache-Control": "public, max-age=60, s-maxage=300",
// max-age=60: browser caches for 60s
// s-maxage=300: CDN caches for 300s
},
});
// Private content (authenticated)
new Response(body, {
headers: {
"Cache-Control": "private, max-age=0, must-revalidate",
},
});
// Stale-while-revalidate: serve stale content while fetching fresh in background
new Response(body, {
headers: {
"Cache-Control": "public, max-age=60, stale-while-revalidate=300",
},
});
ETag and Conditional Requests
function generateETag(content: string): string {
// Simple hash-based ETag
let hash = 0;
for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0;
}
return `"${hash.toString(36)}"`;
}
async function handleWithETag(request: Request, content: string): Promise<Response> {
const etag = generateETag(content);
const ifNoneMatch = request.headers.get("if-none-match");
if (ifNoneMatch === etag) {
return new Response(null, { status: 304 });
}
return new Response(content, {
headers: {
ETag: etag,
"Cache-Control": "public, max-age=0, must-revalidate",
},
});
}
Cloudflare Cache API
Workers can directly read and write to Cloudflare's edge cache:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const cache = caches.default;
const cacheKey = new Request(request.url, { method: "GET" });
// Check cache
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// Fetch from origin
response = await fetch(request);
// Clone and cache the response
const responseToCache = new Response(response.body, response);
responseToCache.headers.set("Cache-Control", "public, max-age=300");
ctx.waitUntil(cache.put(cacheKey, responseToCache.clone()));
return responseToCache;
},
};
Vary Header
Tell the CDN to cache different versions based on request characteristics:
new Response(body, {
headers: {
"Cache-Control": "public, max-age=3600",
Vary: "Accept-Language, Accept-Encoding",
},
});
Implementation Patterns
Tiered Cache with KV Fallback
interface Env {
CACHE_KV: KVNamespace;
}
async function tieredCacheFetch(
request: Request,
env: Env,
ctx: ExecutionContext,
originUrl: string,
ttlSeconds: number
): Promise<Response> {
const cacheKey = new URL(originUrl).pathname;
// Tier 1: Cloudflare edge cache
const edgeCache = caches.default;
const edgeCacheKey = new Request(`https://cache.internal${cacheKey}`);
let response = await edgeCache.match(edgeCacheKey);
if (response) {
response = new Response(response.body, response);
response.headers.set("X-Cache", "HIT-edge");
return response;
}
// Tier 2: KV store (globally replicated)
const kvValue = await env.CACHE_KV.get(cacheKey);
if (kvValue) {
response = new Response(kvValue, {
headers: {
"Content-Type": "application/json",
"Cache-Control": `public, max-age=${ttlSeconds}`,
"X-Cache": "HIT-kv",
},
});
ctx.waitUntil(edgeCache.put(edgeCacheKey, response.clone()));
return response;
}
// Tier 3: Origin
const originResponse = await fetch(originUrl);
const body = await originResponse.text();
response = new Response(body, {
headers: {
"Content-Type": originResponse.headers.get("Content-Type") ?? "application/json",
"Cache-Control": `public, max-age=${ttlSeconds}`,
"X-Cache": "MISS",
},
});
// Populate both cache tiers
ctx.waitUntil(Promise.all([
edgeCache.put(edgeCacheKey, response.clone()),
env.CACHE_KV.put(cacheKey, body, { expirationTtl: ttlSeconds * 2 }),
]));
return response;
}
Cache Purging Pattern
async function purgeCache(
env: Env,
ctx: ExecutionContext,
keys: string[]
): Promise<void> {
const edgeCache = caches.default;
await Promise.all(
keys.map(async (key) => {
const cacheKey = new Request(`https://cache.internal${key}`);
await edgeCache.delete(cacheKey);
await env.CACHE_KV.delete(key);
})
);
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method === "PURGE") {
const authHeader = request.headers.get("Authorization");
if (authHeader !== `Bearer ${env.PURGE_TOKEN}`) {
return new Response("Unauthorized", { status: 401 });
}
const body = await request.json<{ keys: string[] }>();
await purgeCache(env, ctx, body.keys);
return Response.json({ purged: body.keys.length });
}
// ... normal request handling
return new Response("OK");
},
};
Stale-While-Revalidate in Application Code
When the CDN does not support stale-while-revalidate, implement it manually:
interface CachedItem<T> {
data: T;
timestamp: number;
maxAge: number;
staleWhileRevalidate: number;
}
async function swr<T>(
kv: KVNamespace,
key: string,
fetcher: () => Promise<T>,
ctx: ExecutionContext,
maxAge = 60,
staleWindow = 300
): Promise<T> {
const cached = await kv.get<CachedItem<T>>(key, "json");
const now = Date.now();
if (cached) {
const age = (now - cached.timestamp) / 1000;
// Fresh: return immediately
if (age < maxAge) {
return cached.data;
}
// Stale but within revalidation window: return stale, refresh in background
if (age < maxAge + staleWindow) {
ctx.waitUntil(
fetcher().then((data) =>
kv.put(key, JSON.stringify({
data,
timestamp: Date.now(),
maxAge,
staleWhileRevalidate: staleWindow,
}), { expirationTtl: maxAge + staleWindow + 60 })
)
);
return cached.data;
}
}
// No cache or fully expired: fetch synchronously
const data = await fetcher();
await kv.put(key, JSON.stringify({
data,
timestamp: Date.now(),
maxAge,
staleWhileRevalidate: staleWindow,
}), { expirationTtl: maxAge + staleWindow + 60 });
return data;
}
Cache Key Normalization
function normalizeCacheKey(request: Request): string {
const url = new URL(request.url);
// Sort query parameters for consistent cache keys
const params = new URLSearchParams(url.searchParams);
const sortedParams = new URLSearchParams([...params.entries()].sort());
// Strip tracking parameters
sortedParams.delete("utm_source");
sortedParams.delete("utm_medium");
sortedParams.delete("utm_campaign");
sortedParams.delete("fbclid");
sortedParams.delete("gclid");
// Normalize pathname (lowercase, remove trailing slash)
let pathname = url.pathname.toLowerCase();
if (pathname.length > 1 && pathname.endsWith("/")) {
pathname = pathname.slice(0, -1);
}
const queryString = sortedParams.toString();
return `${pathname}${queryString ? "?" + queryString : ""}`;
}
Best Practices
- Use
s-maxageto separate CDN and browser cache durations — browsers often need shorter TTLs than the CDN edge. - Hash static asset filenames — this enables infinite
max-agewithimmutable, since new content gets a new URL. - Implement
stale-while-revalidate— it provides the best user experience by serving cached content instantly while refreshing in the background. - Normalize cache keys — sort query parameters, strip tracking parameters, and lowercase paths to avoid duplicate cache entries.
- Use
Varysparingly — each unique combination ofVaryheader values creates a separate cache entry. Over-varying fragments the cache. - Set
Cache-Control: privateon authenticated responses — prevents CDN from caching user-specific content and serving it to other users. - Monitor cache hit ratios — use CDN analytics to identify low-hit-ratio paths and adjust TTLs or caching strategies accordingly.
Common Pitfalls
- Caching responses with
Set-Cookie— CDNs may strip cookies from cached responses or, worse, serve one user's session cookie to another. Never cache responses that set cookies. - Not varying on
Accept-Encoding— if you serve both compressed and uncompressed responses, omittingVary: Accept-Encodingcauses clients to receive the wrong encoding. - Cache stampede on expiration — when a popular cache entry expires, hundreds of concurrent requests hit the origin simultaneously. Use stale-while-revalidate or locking mechanisms.
- Forgetting to purge after deployments — stale HTML pages referencing old asset hashes cause broken experiences. Purge HTML caches on deploy.
- Using query strings for cache busting on static assets — some CDNs ignore query strings by default. Use filename hashing instead (
app.a1b2c3.js). - Caching error responses — a
500or503from the origin gets cached and served to all users. Always check the status code before caching: only cache 200-level responses.
Core Philosophy
Caching is not an optimization — it is an architecture decision that determines your application's performance, cost, and resilience. The fastest request is one that never reaches the origin. Design your cache strategy as deliberately as your database schema: decide what is cacheable, for how long, under what conditions it is invalidated, and what happens when the cache is cold.
Stale-while-revalidate is the most user-friendly caching pattern. It serves cached content instantly (keeping the user experience fast) while refreshing in the background (keeping content fresh). The user gets the best of both worlds: speed and accuracy. Implement this pattern at the edge for content that changes periodically but does not need to be instantly fresh.
Cache keys are the hidden complexity of edge caching. Two requests to the same URL can produce different responses based on headers (Accept-Language, Accept-Encoding), cookies (auth state), query parameters (tracking parameters), and path casing. Normalize your cache keys — sort query parameters, strip tracking params, lowercase paths — to maximize cache hit rates and avoid serving the wrong content.
Anti-Patterns
-
Caching responses with
Set-Cookieheaders — CDNs may serve one user's session cookie to another; never cache responses that set cookies or contain user-specific data. -
Setting
Cache-Control: publicon authenticated responses — this allows CDN edge nodes to serve user-specific content to other users; authenticated responses must useCache-Control: privateor no-store. -
Using query strings for cache busting — some CDNs ignore query strings by default; use content-hashed filenames (
app.a1b2c3.js) instead ofapp.js?v=123for reliable cache busting. -
Forgetting to purge HTML caches after deployments — stale HTML pages referencing old asset hashes produce broken user experiences with missing JavaScript and CSS.
-
Over-using the
Varyheader — each unique combination ofVaryheader values creates a separate cache entry; varying on too many headers fragments the cache and reduces hit rates dramatically.
Install this skill directly: skilldb add edge-computing-skills
Related Skills
Cloudflare D1
Cloudflare D1 for running SQLite databases at the edge with SQL query support
Cloudflare Kv
Cloudflare Workers KV for globally distributed key-value storage at the edge
Cloudflare Workers
Cloudflare Workers for serverless edge compute using the V8 isolate model
Deno Deploy
Deno Deploy for globally distributed edge applications using the Deno runtime
Edge Auth
Authentication and authorization at the edge for securing requests before they reach the origin
Geolocation Routing
Geo-based routing and personalization for delivering localized content at the edge