Summary
The CloudFront cache key built by sst.aws.Nextjs does not include the
next-router-segment-prefetch header. A request for a route's segment tree and a request for
its full page therefore hash to the same cache key, so CloudFront answers segment-tree requests
with the cached full-page RSC payload.
On Next 16 this is not merely "a bigger payload". For a build-time prerendered route the
full-page payload carries PrefetchHint.InliningHintsStale, which instructs the client router
to expire the cache entry immediately and re-fetch the corrected tree. That correction can never
arrive, because the corrective request maps to the same cache key and returns the same flagged
payload. The client re-prefetches indefinitely.
Measured on a deployed app: 63–180 requests/second, per open tab, indefinitely, all for the
same URL and the same _rsc token, from every page that links to the prerendered route.
Environment
|
|
sst |
4.17.1 |
next |
16.3.0 |
| Component |
sst.aws.Nextjs |
open-next.config.ts |
enableCacheInterception not set (defaults off) |
The defect
.sst/platform/src/components/aws/router.ts:2842:
var headers = ["rsc","next-router-prefetch","next-router-state-tree","next-url","x-prerender-revalidate"];
for (var i=0; i<headers.length; i++) cacheKey += getHeader(headers[i]);
Next's CDN caching guide documents that App
Router responses vary on rsc, next-router-state-tree, next-router-prefetch,
next-router-segment-prefetch and next-url. The array above is that list minus
next-router-segment-prefetch.
The deployed cache policy whitelists only the resulting hash, so nothing else re-introduces the
distinction:
Name: <stack>SiteServerCachePolicy-* Comment: "SST server response cache policy"
HeadersConfig: whitelist -> ["x-open-next-cache-key", "x-forwarded-host"]
CookiesConfig: none
QueryStringsConfig: all
MinTTL 0 / DefaultTTL 0 / MaxTTL 31536000
Evidence
The build emits two artefacts for the prerendered route /:
| artefact |
size |
prefetchHints |
.next/server/app/index.rsc |
10,075 B |
contains 4608 (4096 + 512 = InliningHintsStale) |
.next/server/app/index.segments/_tree.segment.rsc |
417 B |
4176 / 4256 — no 512 bit |
Two requests to the deployed site differing only in the segment-prefetch header returned
byte-identical responses:
curl -s "https://<host>/?_rsc=<token>" \
-H 'RSC: 1' -H 'Next-Router-Prefetch: 1' -H 'Next-Url: /users'
# 6532 bytes, full-page payload beginning 1:"$Sreact.fragment" / ClientPageRoot
curl -s "https://<host>/?_rsc=<token>" \
-H 'RSC: 1' -H 'Next-Router-Prefetch: 1' -H 'Next-Url: /users' \
-H 'Next-Router-Segment-Prefetch: /_tree'
# 6532 bytes, identical — the 417 B tree artefact is never served
Response headers on both:
x-nextjs-prerender: 1,1
x-nextjs-stale-time: 300
cache-control: s-maxage=31536000
x-cache: Hit from cloudfront
age: 3348
Control: a force-dynamic route in the same app returns cache-control: private, no-cache, no-store, is an x-cache: Miss from cloudfront, and correctly returns its 323 B tree payload
(0:{"f":[...]}). So the origin honours the header; only cached responses collapse the
distinction.
Client side, next/dist/client/components/segment-cache/cache.js (~line 939):
if (tree.prefetchHints & PrefetchHint.InliningHintsStale) {
fulfilledEntry.staleAt = -1; // always stale -> re-fetch immediately
} else {
fulfilledEntry.staleAt = now + STATIC_STALETIME_MS;
}
InliningHintsStale is set server-side in
next/dist/server/app-render/create-flight-router-state-from-loader-tree.js for a build-time
prerender generated before collectPrefetchHints runs. The sibling branch in that same function
carries the warning that makes the severity plain:
Do NOT set InliningHintsStale — that would cause the client to enter an infinite re-fetch loop
trying to get hints that will never exist.
Why this was presumably omitted
Next's CDN guide lists next-router-segment-prefetch under "What you can safely ignore":
when omitted on prefetch requests, the server falls back to a broader prefetch payload instead
of a segment-specific one
That is accurate for dynamic routes and benign there. It does not hold for build-time prerendered
routes, where the "broader payload" is the flagged one and the segment request is the documented
recovery path. So this looks like SST correctly following upstream guidance that has a gap, rather
than an oversight — but the consequence on Next 16 is a production traffic loop.
Reproduction
- Deploy any Next 16 app via
sst.aws.Nextjs containing a route that is not force-dynamic
(so it is build-time prerendered) and is linked with <Link> from a component rendered on
every page — a sidebar logo or breadcrumb home icon is the natural case.
- Load any other page and watch the network panel.
- Requests for the prerendered route's URL repeat indefinitely at roughly one per animation
frame. No console errors. It pauses while the tab is not painting, which makes it easy to miss.
Suggested fix
Add the header to the array in router.ts:
-var headers = ["rsc","next-router-prefetch","next-router-state-tree","next-url","x-prerender-revalidate"];
+var headers = ["rsc","next-router-prefetch","next-router-segment-prefetch","next-router-state-tree","next-url","x-prerender-revalidate"];
This trades a small amount of cache fragmentation (segment-tree responses cached separately from
full-page responses — which is the correct behaviour) for eliminating the loop.
Workaround for anyone hitting this
Ensure nothing links to a build-time prerendered route: either point shell links at a
force-dynamic route, or set prefetch={false} on those links. Making the route dynamic also
works, but note that route segment config is silently ignored in a 'use client' page — it needs
a server component.
Related
- opennextjs-aws#1212 — same symptom
(segment prefetches never served, unbounded loop) via a different cause, a truthiness check on
prefetchInlining which Next 16 changed from boolean to object. Not applicable here:
enableCacheInterception is off.
- vercel/next.js#85489 — bounded 2–3×
duplicate prefetches on Next 16. Distinct from this unbounded case.
- anomalyco/sst#6404 —
setNextjsCacheKey() not
reached for image-optimizer routes. Same function, different defect.
Known gap in this report
We could not directly observe the origin returning the tree artefact for a segment request,
because every unauthenticated request to our deployment is redirected by Clerk middleware before
reaching Next. The inference that the origin would return the tree rests on (a) the artefact
existing in the build output and (b) the force-dynamic control route returning its tree
correctly on a cache miss.
Investigation and initial draft were AI-assisted. All measurements were taken against a live
deployment; the CloudFront cache policy and viewer-request function were read via the AWS CLI,
and the client/server behaviour was verified against the installed next@16.3.0 and sst@4.17.1
sources.
Summary
The CloudFront cache key built by
sst.aws.Nextjsdoes not include thenext-router-segment-prefetchheader. A request for a route's segment tree and a request forits full page therefore hash to the same cache key, so CloudFront answers segment-tree requests
with the cached full-page RSC payload.
On Next 16 this is not merely "a bigger payload". For a build-time prerendered route the
full-page payload carries
PrefetchHint.InliningHintsStale, which instructs the client routerto expire the cache entry immediately and re-fetch the corrected tree. That correction can never
arrive, because the corrective request maps to the same cache key and returns the same flagged
payload. The client re-prefetches indefinitely.
Measured on a deployed app: 63–180 requests/second, per open tab, indefinitely, all for the
same URL and the same
_rsctoken, from every page that links to the prerendered route.Environment
sstnextsst.aws.Nextjsopen-next.config.tsenableCacheInterceptionnot set (defaults off)The defect
.sst/platform/src/components/aws/router.ts:2842:Next's CDN caching guide documents that App
Router responses vary on
rsc,next-router-state-tree,next-router-prefetch,next-router-segment-prefetchandnext-url. The array above is that list minusnext-router-segment-prefetch.The deployed cache policy whitelists only the resulting hash, so nothing else re-introduces the
distinction:
Evidence
The build emits two artefacts for the prerendered route
/:prefetchHints.next/server/app/index.rscInliningHintsStale).next/server/app/index.segments/_tree.segment.rscTwo requests to the deployed site differing only in the segment-prefetch header returned
byte-identical responses:
Response headers on both:
Control: a
force-dynamicroute in the same app returnscache-control: private, no-cache, no-store, is anx-cache: Miss from cloudfront, and correctly returns its 323 B tree payload(
0:{"f":[...]}). So the origin honours the header; only cached responses collapse thedistinction.
Client side,
next/dist/client/components/segment-cache/cache.js(~line 939):InliningHintsStaleis set server-side innext/dist/server/app-render/create-flight-router-state-from-loader-tree.jsfor a build-timeprerender generated before
collectPrefetchHintsruns. The sibling branch in that same functioncarries the warning that makes the severity plain:
Why this was presumably omitted
Next's CDN guide lists
next-router-segment-prefetchunder "What you can safely ignore":That is accurate for dynamic routes and benign there. It does not hold for build-time prerendered
routes, where the "broader payload" is the flagged one and the segment request is the documented
recovery path. So this looks like SST correctly following upstream guidance that has a gap, rather
than an oversight — but the consequence on Next 16 is a production traffic loop.
Reproduction
sst.aws.Nextjscontaining a route that is notforce-dynamic(so it is build-time prerendered) and is linked with
<Link>from a component rendered onevery page — a sidebar logo or breadcrumb home icon is the natural case.
frame. No console errors. It pauses while the tab is not painting, which makes it easy to miss.
Suggested fix
Add the header to the array in
router.ts:This trades a small amount of cache fragmentation (segment-tree responses cached separately from
full-page responses — which is the correct behaviour) for eliminating the loop.
Workaround for anyone hitting this
Ensure nothing links to a build-time prerendered route: either point shell links at a
force-dynamicroute, or setprefetch={false}on those links. Making the route dynamic alsoworks, but note that route segment config is silently ignored in a
'use client'page — it needsa server component.
Related
(segment prefetches never served, unbounded loop) via a different cause, a truthiness check on
prefetchInliningwhich Next 16 changed from boolean to object. Not applicable here:enableCacheInterceptionis off.duplicate prefetches on Next 16. Distinct from this unbounded case.
setNextjsCacheKey()notreached for image-optimizer routes. Same function, different defect.
Known gap in this report
We could not directly observe the origin returning the tree artefact for a segment request,
because every unauthenticated request to our deployment is redirected by Clerk middleware before
reaching Next. The inference that the origin would return the tree rests on (a) the artefact
existing in the build output and (b) the
force-dynamiccontrol route returning its treecorrectly on a cache miss.
Investigation and initial draft were AI-assisted. All measurements were taken against a live
deployment; the CloudFront cache policy and viewer-request function were read via the AWS CLI,
and the client/server behaviour was verified against the installed
next@16.3.0andsst@4.17.1sources.