@@ -236,6 +236,8 @@ Caching is essential for performant server-rendered applications, but current so
236236
237237** Important:** This API only works in on-demand SSR routes. Static and prerendered routes are already cached and don't support route-level cache control. Caching is not active during development — the API is available but calls are no-ops, ensuring the dev server always serves fresh content.
238238
239+ ** When cache is not configured:** If no cache provider is configured (i.e. no ` cache ` in config and the adapter does not provide a default), accessing ` Astro.cache ` throws a ` CacheNotEnabled ` error with an actionable message directing the user to configure a cache provider.
240+
239241### ` Astro.cache.set(options | cacheHint | entry | false) `
240242
241243Declares caching behavior for the current route. Sets the appropriate headers for the current adapter.
@@ -331,10 +333,11 @@ Invalidates cached responses. Available in `.astro` pages, API routes, and middl
331333** Invalidate by path:**
332334
333335``` ts
334- cache .invalidate ({ path: " /products/laptop" });
335- cache .invalidate ({ path: " /blog/*" }); // Pattern matching
336+ cache .invalidate ({ path: " /products/laptop" }); // Exact path
336337```
337338
339+ Note: The current implementation supports exact-path invalidation. Wildcard patterns are not currently supported.
340+
338341** Invalidate by tag:**
339342
340343``` ts
@@ -376,9 +379,17 @@ interface CacheProvider {
376379 * - Read cache headers from response to determine caching behavior
377380 * - Store response in cache
378381 * - Return response
382+ *
383+ * The context includes an optional `waitUntil` function, which runtime
384+ * providers can use for SWR background revalidation without blocking
385+ * the response.
379386 */
380387 onRequest? (
381- context : MiddlewareContext ,
388+ context : {
389+ request: Request ;
390+ url: URL ;
391+ waitUntil? : (p : Promise <unknown >) => void ;
392+ },
382393 next : () => Promise <Response >,
383394 ): Promise <Response >;
384395
@@ -420,6 +431,11 @@ interface AstroCache {
420431 */
421432 readonly tags: string [];
422433
434+ /**
435+ * Current accumulated cache options for this request (read-only).
436+ */
437+ readonly options: Readonly <CacheOptions >;
438+
423439 /**
424440 * Invalidate cached content by path, tags, or entry.
425441 */
@@ -449,6 +465,7 @@ There are two fundamental types of cache providers:
449465** Runtime provider middleware flow:**
450466
451467``` ts
468+ // context: { request: Request; url: URL; waitUntil?: (p: Promise<unknown>) => void }
452469async onRequest (context , next ) {
453470 // 1. Check cache using context.request. Assume this.cache is a simple key-value store.
454471 const cached = await this .cache .get (context .url .pathname );
@@ -487,28 +504,30 @@ async onRequest(context, next) {
487504
488505This design ensures consistent behavior: runtime providers honour the same cache headers that external CDNs use.
489506
507+ ** Observability:** Runtime providers set an ` X-Astro-Cache ` response header with the value ` HIT ` , ` MISS ` , or ` STALE ` to indicate whether the response was served from cache. This is useful for debugging and monitoring cache behavior.
508+
490509### Example Runtime Provider Implementation
491510
492511Here's a complete example of a Node memory provider implementation:
493512
494513``` ts
495- import { LRUCache } from " lru-cache" ;
496-
514+ // Zero-dependency in-memory LRU cache implementation
497515class NodeMemoryProvider implements CacheProvider {
498516 name = " node-memory" ;
499- private cache: LRUCache <string , CachedEntry >;
500-
501- constructor (options ? : { max? : number ; ttl? : number }) {
502- this .cache = new LRUCache ({
503- max: options ?.max ?? 500 ,
504- ttl: options ?.ttl ?? 1000 * 60 * 5 , // 5 minutes default
505- allowStale: true ,
506- updateAgeOnGet: true ,
507- });
517+ private cache: Map <string , CachedEntry >;
518+ private max: number ;
519+
520+ constructor (options ? : { max? : number }) {
521+ this .max = options ?.max ?? 1000 ;
522+ this .cache = new Map ();
508523 }
509524
510525 async onRequest(
511- context : CacheMiddlewareContext ,
526+ context : {
527+ request: Request ;
528+ url: URL ;
529+ waitUntil? : (p : Promise <unknown >) => void ;
530+ },
512531 next : () => Promise <Response >,
513532 ): Promise <Response > {
514533 // In real implementation, normalise the URL params etc
@@ -523,13 +542,13 @@ class NodeMemoryProvider implements CacheProvider {
523542 // Get response from route
524543 const response = await next ();
525544
526- // Parse cache headers
545+ // Parse cache headers — TTL is per-entry from response headers
527546 const cacheControl = response .headers .get (" CDN-Cache-Control" );
528547 if (cacheControl ) {
529548 const { maxAge, swr } = this .parseCacheControl (cacheControl );
530549 const tags = response .headers .get (" Cache-Tag" )?.split (" , " ) ?? [];
531550
532- // Store in cache
551+ // Store in cache (evict LRU entries if at capacity)
533552 this .cache .set (cacheKey , {
534553 response: response .clone (),
535554 tags ,
@@ -543,16 +562,9 @@ class NodeMemoryProvider implements CacheProvider {
543562 }
544563
545564 async invalidate(options : InvalidateOptions ): Promise <void > {
546- // Naive implementation iterates all keys
547-
548565 if (options .path ) {
549- // Invalidate by path (with wildcard support)
550- const pattern = new URLPattern (options .path );
551- for (const key of this .cache .keys ()) {
552- if (pattern .test (key )) {
553- this .cache .delete (key );
554- }
555- }
566+ // Exact-path invalidation (wildcard patterns not currently supported)
567+ this .cache .delete (options .path );
556568 }
557569
558570 if (options .tags ) {
@@ -572,8 +584,8 @@ This example demonstrates:
572584
573585- Middleware-style ` onRequest() ` that checks cache before calling ` next() `
574586- Reading cache headers from the response to determine caching behavior
575- - Storing responses with metadata (tags, TTL, timestamp )
576- - Invalidation by both path patterns and cache tags
587+ - Per-entry TTL derived from response cache headers (not a global default )
588+ - Exact- path invalidation and tag-based invalidation
577589- Proper response cloning to avoid consuming the stream
578590
579591### Default Header Generation
@@ -586,15 +598,17 @@ function defaultSetHeaders(options: CacheOptions): Headers {
586598 const headers = new Headers ();
587599
588600 // Build Cache-Control value
589- const directives = [" public " ];
601+ const directives: string [] = [];
590602 if (options .maxAge !== undefined ) {
591603 directives .push (` max-age=${options .maxAge } ` );
592604 }
593605 if (options .swr !== undefined ) {
594606 directives .push (` stale-while-revalidate=${options .swr } ` );
595607 }
596608
597- headers .set (" CDN-Cache-Control" , directives .join (" , " ));
609+ if (directives .length ) {
610+ headers .set (" CDN-Cache-Control" , directives .join (" , " ));
611+ }
598612
599613 if (options .tags ?.length ) {
600614 headers .set (" Cache-Tag" , options .tags .join (" , " ));
@@ -649,7 +663,7 @@ Provider packages export a function that accepts provider-specific options and r
649663export function cacheFastly(options : FastlyOptions ) {
650664 return {
651665 entrypoint: new URL (" ./provider.js" , import .meta .url ),
652- options ,
666+ config: options ,
653667 };
654668}
655669```
@@ -665,15 +679,15 @@ Each cache provider maps `Astro.cache.set()` options to platform-specific header
665679** Vercel Provider (` vercel ` ):**
666680
667681```
668- maxAge + swr → CDN-Cache-Control: public, max-age=300, stale-while-revalidate=3600
682+ maxAge + swr → CDN-Cache-Control: max-age=300, stale-while-revalidate=3600
669683tags → Cache-Tag: products, product:123
670684lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
671685```
672686
673687** Netlify Provider:**
674688
675689```
676- maxAge + swr → Netlify-CDN-Cache-Control: public, max-age=300, stale-while-revalidate=3600, durable
690+ maxAge + swr → Netlify-CDN-Cache-Control: max-age=300, stale-while-revalidate=3600, durable
677691tags → Netlify-Cache-Tag: products, product:123
678692lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
679693```
@@ -685,7 +699,7 @@ Uses Cloudflare's upcoming support for caching Worker responses with native tag-
685699For use when Cloudflare acts as a CDN/proxy in front of another origin (like Node.js):
686700
687701```
688- maxAge + swr → Cache-Control: public, s-maxage=300, stale-while-revalidate=3600
702+ maxAge + swr → Cache-Control: s-maxage=300, stale-while-revalidate=3600
689703tags → Cache-Tag: products, product:123
690704lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
691705```
@@ -705,7 +719,7 @@ Note: Fastly uses `Surrogate-Control` and `Surrogate-Key` headers (proprietary F
705719** Akamai Provider:**
706720
707721```
708- maxAge + swr → CDN-Cache-Control: public, max-age=300, stale-while-revalidate=3600
722+ maxAge + swr → CDN-Cache-Control: max-age=300, stale-while-revalidate=3600
709723tags → Edge-Cache-Tag: products,product:123
710724lastModified → Last-Modified: Thu, 15 Jan 2025 00:00:00 GMT
711725```
@@ -714,7 +728,7 @@ Note: Akamai supports the standardized `CDN-Cache-Control` header (RFC 9213) for
714728
715729** Node Memory Provider:**
716730
717- In- memory LRU cache using ` lru-cache ` library with stale-while-revalidate support. As a runtime provider, it reads the ` CDN-Cache-Control ` and ` Cache-Tag ` headers set by the default header generation to determine caching behavior.
731+ Zero-dependency in- memory LRU cache with stale-while-revalidate support. As a runtime provider, it reads the ` CDN-Cache-Control ` and ` Cache-Tag ` headers set by the default header generation to determine caching behavior. TTL is determined per-entry from the response's cache headers rather than a global default .
718732
719733### Invalidation Implementation
720734
@@ -852,7 +866,7 @@ Provider packages export a configuration function that accepts type-safe options
852866export function cacheFastly(options : FastlyOptions ) {
853867 return {
854868 entrypoint: new URL (" ./provider.js" , import .meta .url ),
855- options ,
869+ config: options ,
856870 };
857871}
858872
@@ -997,8 +1011,7 @@ export default defineConfig({
9971011 adapter: node (),
9981012 cache: {
9991013 provider: cacheMemory ({
1000- max: 1000 , // Max cache entries
1001- ttl: 300000 , // Default TTL in ms
1014+ max: 1000 , // Max cache entries (default: 1000)
10021015 }),
10031016 },
10041017});
@@ -1114,12 +1127,13 @@ Cache set in middleware is treated like config-level defaults: `Astro.cache.set(
11141127
11151128## Node.js Implementation Details
11161129
1117- The Node adapter maintains an in-memory LRU cache which provides:
1130+ The Node adapter maintains a zero-dependency in-memory LRU cache (default max: 1000 entries) which provides:
11181131
11191132- Automatic eviction of least-recently-used entries
1120- - Built-in stale-while-revalidate support via ` fetchMethod `
1121- - TTL management
1133+ - Built-in stale-while-revalidate support
1134+ - Per-entry TTL derived from response cache headers
11221135- Size limits to prevent memory issues
1136+ - Exact-path invalidation (wildcard patterns not currently supported)
11231137
11241138** Important limitations:**
11251139
@@ -1225,7 +1239,6 @@ export default defineConfig({
12251239 cache: {
12261240 provider: cacheMemory ({
12271241 max: 1000 ,
1228- ttl: 300000 ,
12291242 }),
12301243 },
12311244});
@@ -1328,8 +1341,8 @@ Users currently setting cache headers manually can migrate incrementally:
13281341
13291342``` astro
13301343---
1331- Astro.response.headers.set('Cache-Control', 'public, max-age=300');
1332- Astro.response.headers.set('CDN-Cache-Control', 'public, max-age=300, stale-while-revalidate=3600');
1344+ Astro.response.headers.set('Cache-Control', 'max-age=300');
1345+ Astro.response.headers.set('CDN-Cache-Control', 'max-age=300, stale-while-revalidate=3600');
13331346---
13341347```
13351348
0 commit comments