You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Astro.cache.set() in layouts and components is silently dropped once rendering has awaited #18453
Astro v7.4.0
Node v24.21.0
System Linux (x64)
Package Manager npm
Output server
Adapter @astrojs/node (v11.1.8)
Integrations none
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
With memoryCache(), tags set via Astro.cache.set() in a layout or component are silently ignored once rendering has awaited outside the page frontmatter. Invalidating such a tag does not re-render the page:
page-tag ok: re-rendered
page-tag-after-await ok: re-rendered
layout-tag ok: re-rendered
layout-tag-after-await BUG: still served from cache
component-tag BUG: still served from cache
component-tag-after-await BUG: still served from cache
The cache handler reads the cache options as soon as the streamed response is created (applyCacheHeaders() in dist/core/cache/handler.js), so later directives still show up in Astro.cache.tags but never reach the cache. Since memoryCache() buffers the whole body before storing anyway, it could read them once rendering has finished. Details and a workaround are in the repro README.
What's the expected result?
Invalidating any tag the page set re-renders it, as the caching guide says layouts and page code "can each contribute cache directives independently" - or at least a warning when cache.set() is called too late to take effect.
Unit Test: Yes — packages/astro/test/units/cache/app-cache.test.ts
Priority:P2: has workaround. This goes against the caching guide, which says layouts can add their own cache directives. The middleware workaround in the repro README fixes it (I confirmed it does).
handleCache() in packages/astro/src/core/cache/handler.ts calls applyCacheHeaders() as soon as next() resolves. For a streamed page, that happens before layouts (after their first await) and child components have rendered. memoryCache() then takes maxAge/tags from those early CDN-Cache-Control/Cache-Tag headers. Any later Astro.cache.set() calls still show up in Astro.cache.tags but never reach the cache, and nothing is logged.
I found a potential fix for this issue. It only changes the runtime-provider (onRequest) path. If the request is a GET with a body and maxAge > 0 is already set, the handler reads the full body before applying cache headers. That keeps the Astro cookies attached to the response. memoryCache() already waits for the full body before storing cacheable responses, so cacheable pages don't lose streaming. Uncacheable pages, non-GET requests and CDN providers stream as before. This needs human review. Two limitations remain: a maxAge set only late (e.g. only in a component after an await) is still not picked up, and CDN providers still read headers before the body streams.
Try this fix
You can test this fix right now without waiting for a release:
# astro
npm i https://pkg.pr.new/astro@46dcb2d
If this fixes your issue, please leave a comment letting us know (for example
"confirmed, this fixes it"). A pull request will then be opened to get it merged.
Full Triage Report
Issue summary
With cache: { provider: memoryCache() }, output: 'server' and @astrojs/node (standalone), tags set through Astro.cache.set() in a layout or component are dropped once rendering has awaited outside the page frontmatter. Invalidating those tags doesn't re-render the page. Expected behavior, per the caching guide ("Middleware, layouts, content loaders, and page code can each contribute cache directives independently"): invalidating any tag the page set re-renders it, or at least a warning is logged.
Reported environment: Astro 7.4.0, @astrojs/node 11.1.8, Node 24.21.0, Linux, npm.
Tested against: current main (astro 7.4.1, @astrojs/node 11.1.9 from the workspace), Node 22+.
the page sets maxAge: 3600 + page-tag, then awaits, then sets page-tag-after-await
the layout sets layout-tag, then awaits, then sets layout-tag-after-await
the child component sets component-tag, then awaits, then sets component-tag-after-await
POST /api/invalidate?tag=… calls context.cache.invalidate({ tags: [tag] })
Steps:
astro build
node dist/server/entry.mjs
node repro.mjs (for each tag: prime the cache with MISS then HIT, invalidate the tag, check whether the page re-renders)
Result on main (reproduces every time):
page-tag MISS -> HIT -> invalidate -> MISS ok: re-rendered
page-tag-after-await MISS -> HIT -> invalidate -> MISS ok: re-rendered
layout-tag MISS -> HIT -> invalidate -> MISS ok: re-rendered
layout-tag-after-await MISS -> HIT -> invalidate -> HIT BUG: still served from cache
component-tag MISS -> HIT -> invalidate -> HIT BUG: still served from cache
component-tag-after-await MISS -> HIT -> invalidate -> HIT BUG: still served from cache
Nothing is logged. With the reporter's buffering-middleware workaround in place, all 6 tags re-render as expected.
Diagnosis
I logged the tags inside AstroCache[APPLY_HEADERS]. On every cache miss it only sees ['page-tag', 'page-tag-after-await', 'layout-tag'].
core/cache/handler.tshandleCache(): for providers with onRequest, the wrapped next runs const res = await next(); applyCacheHeaders(cache, res, request). The CDN path does the same thing at the end of the function.
runtime/server/render/page.ts / runtime/server/render/astro/render.ts: next() resolves once the page frontmatter and head have rendered and a streaming body exists. The rest of the component tree renders lazily as the body is read.
core/cache/runtime/cache.ts[APPLY_HEADERS]: takes a snapshot of the options as CDN-Cache-Control / Cache-Tag. Later set() calls still update the internal state but never reach the headers.
core/cache/memory-provider.tsonRequest: on a miss, and on SWR revalidation, it reads maxAge/swr/tags from those headers beforeserializeResponse() buffers the body.
Middleware runs inside handleCache (core/routing/handler.ts, runPipeline). That's why buffering in user middleware works around it.
Confidence: high.
Verification
Verdict: bug (medium confidence).
The docs say layouts and page code can contribute cache directives independently. Neither the docs nor the JSDoc mention any timing limit.
There's no code comment, guard or warning about late set() calls. Route caching was added in feat: route caching #15579, and its commit message and the stabilisation changeset don't mention this.
The cache test fixtures only call cache.set from API routes, never from layouts or components.
Caveat: Astro already has a known streaming limitation for response headers and cookies in components, and a maintainer could see this as the same thing. For CDN providers, late directives can't be honoured without buffering the page.
Fix
packages/astro/src/core/cache/handler.ts:
+import { attachCookiesToResponse, getCookiesFromResponse } from '../cookies/response.js';
...
async () => {
- const res = await next();+ let res = await next();+ // `next()` resolves once the streamed body starts, but layouts and+ // components can still call `cache.set()` while the rest renders.+ // When the response is going to be cached, the provider buffers the+ // body anyway, so render it fully before reading the cache options.+ // https://github.com/withastro/astro/issues/18453+ if (state.request.method === 'GET' && res.body && (cache!.options.maxAge ?? 0) > 0) {+ const buffered = new Response(await res.arrayBuffer(), res);+ const cookies = getCookiesFromResponse(res);+ if (cookies) attachCookiesToResponse(buffered, cookies);+ res = buffered;+ }
applyCacheHeaders(cache!, res, state.request);
return res;
},
Repro after the fix: all 6 tags show MISS -> HIT -> invalidate -> MISS ok: re-rendered.
New unit test stores tags set by components after rendering has awaited: the page sets maxAge, and a child component awaits and then sets tag late. Invalidating late must produce a MISS. The test fails without the fix (actual: 'HIT') and passes with it.
Regressions: test/units/cache/* 171/171 and test/units/{app,middleware}/** 357/357 pass. Biome and tsc are clean.
Always buffer: breaks streaming for uncacheable pages, and infinite-stream GET endpoints would never finish.
Have the provider re-read the options after reading the body: needs a new channel between the cache object and the provider, and still doesn't handle a maxAge set late.
Only log a warning: doesn't give runtime providers the behavior the docs describe.
Remaining limitations:
A maxAge set only late is still not honoured.
CDN providers could get a warning when cache.set() comes too late, or a docs note.
This report was made by an LLM. The analysis may be wrong, and the potential fix might not work, but is intended as a starting point for exploring the issue.
Activity
astro-factory commentedon Oct 19, 2026
Triage complete
triage: fix pendingpackages/astro/test/units/cache/app-cache.test.tshandleCache()inpackages/astro/src/core/cache/handler.tscallsapplyCacheHeaders()as soon asnext()resolves. For a streamed page, that happens before layouts (after their firstawait) and child components have rendered.memoryCache()then takesmaxAge/tags from those earlyCDN-Cache-Control/Cache-Tagheaders. Any laterAstro.cache.set()calls still show up inAstro.cache.tagsbut never reach the cache, and nothing is logged.I found a potential fix for this issue. It only changes the runtime-provider (
onRequest) path. If the request is a GET with a body andmaxAge > 0is already set, the handler reads the full body before applying cache headers. That keeps the Astro cookies attached to the response.memoryCache()already waits for the full body before storing cacheable responses, so cacheable pages don't lose streaming. Uncacheable pages, non-GET requests and CDN providers stream as before. This needs human review. Two limitations remain: amaxAgeset only late (e.g. only in a component after anawait) is still not picked up, and CDN providers still read headers before the body streams.Try this fix
You can test this fix right now without waiting for a release:
# astro npm i https://pkg.pr.new/astro@46dcb2dIf this fixes your issue, please leave a comment letting us know (for example
"confirmed, this fixes it"). A pull request will then be opened to get it merged.
Full Triage Report
Issue summary
With
cache: { provider: memoryCache() },output: 'server'and@astrojs/node(standalone), tags set throughAstro.cache.set()in a layout or component are dropped once rendering has awaited outside the page frontmatter. Invalidating those tags doesn't re-render the page. Expected behavior, per the caching guide ("Middleware, layouts, content loaders, and page code can each contribute cache directives independently"): invalidating any tag the page set re-renders it, or at least a warning is logged.Reported environment: Astro 7.4.0, @astrojs/node 11.1.8, Node 24.21.0, Linux, npm.
Tested against: current
main(astro 7.4.1, @astrojs/node 11.1.9 from the workspace), Node 22+.Reproduction
Used https://github.com/stefanprobst/astro-cache-tags-repro with the workspace versions of astro and @astrojs/node:
maxAge: 3600+page-tag, thenawaits, then setspage-tag-after-awaitlayout-tag, thenawaits, then setslayout-tag-after-awaitcomponent-tag, thenawaits, then setscomponent-tag-after-awaitPOST /api/invalidate?tag=…callscontext.cache.invalidate({ tags: [tag] })Steps:
astro buildnode dist/server/entry.mjsnode repro.mjs(for each tag: prime the cache with MISS then HIT, invalidate the tag, check whether the page re-renders)Result on
main(reproduces every time):Nothing is logged. With the reporter's buffering-middleware workaround in place, all 6 tags re-render as expected.
Diagnosis
I logged the tags inside
AstroCache[APPLY_HEADERS]. On every cache miss it only sees['page-tag', 'page-tag-after-await', 'layout-tag'].core/cache/handler.tshandleCache(): for providers withonRequest, the wrappednextrunsconst res = await next(); applyCacheHeaders(cache, res, request). The CDN path does the same thing at the end of the function.runtime/server/render/page.ts/runtime/server/render/astro/render.ts:next()resolves once the page frontmatter and head have rendered and a streaming body exists. The rest of the component tree renders lazily as the body is read.core/cache/runtime/cache.ts[APPLY_HEADERS]: takes a snapshot of the options asCDN-Cache-Control/Cache-Tag. Laterset()calls still update the internal state but never reach the headers.core/cache/memory-provider.tsonRequest: on a miss, and on SWR revalidation, it readsmaxAge/swr/tags from those headers beforeserializeResponse()buffers the body.Middleware runs inside
handleCache(core/routing/handler.ts,runPipeline). That's why buffering in user middleware works around it.Confidence: high.
Verification
Verdict: bug (medium confidence).
set()calls. Route caching was added in feat: route caching #15579, and its commit message and the stabilisation changeset don't mention this.cache.setfrom API routes, never from layouts or components.Fix
packages/astro/src/core/cache/handler.ts:MISS -> HIT -> invalidate -> MISS ok: re-rendered.stores tags set by components after rendering has awaited: the page setsmaxAge, and a child component awaits and then sets taglate. Invalidatinglatemust produce a MISS. The test fails without the fix (actual: 'HIT') and passes with it.test/units/cache/*171/171 andtest/units/{app,middleware}/**357/357 pass. Biome andtscare clean..changeset/ten-boxes-wish.md(astro: patch).Alternatives considered:
maxAgeset late.Remaining limitations:
maxAgeset only late is still not honoured.cache.set()comes too late, or a docs note.This report was made by an LLM. The analysis may be wrong, and the potential fix might not work, but is intended as a starting point for exploring the issue.
ematipico commentedon Oct 24, 2026
@stefanprobst please test the preview and tell us if it fixes the issue
Princesseuh commentedon Oct 26, 2026
Closing in favor of #18499 which has more details