Skip to main content

September 15, 2026

Nested remote cache breaks Next.js prerender

Homepage prerender on a Centra Next.js build failed with 'Filling a cache during prerender timed out'. Nested use cache remote boundaries caused the hang.

Sander Korf4 min read

A Centra fashion storefront on Next.js App Router pre-renders the homepage and CMS content pages into static shells. Product cards, carousels, Shop the Look blocks (outfit photos with shoppable hotspots), and review strips hydrate from Centra GraphQL and Sanity. I tagged outer catalog loaders with 'use cache: remote' so editorial churn does not refetch the whole tree on every request.

Deploy failed mid-build with Filling a cache during prerender timed out. Not a flaky network blip. The prerender worker sat inside one remote cache fill while another remote cache fill waited behind it until the clock ran out. Local dev felt fine because I was not exercising the same static prerender path on every save. next dev happily streamed the homepage while I edited Sanity blocks. The failure only showed up when the release pipeline tried to bake the same tree into HTML for edge cache. I spent an afternoon blaming Centra rate limits before I noticed the stack trace always stopped inside our own 'use cache: remote' helper, not inside GraphQL.

Remote cache stacked on remote cache

The homepage loader already ran under 'use cache: remote'. Hydration then fanned out: colour siblings on a card sometimes need a Centra productNumber search when variant relations are thin, content-page display products need a lookup, business reviews need a cached strip, video blocks need their own tagged loader. I had wrapped every Centra read in 'use cache: remote' for consistency.

Nested remote cache means a 'use cache: remote' function called from inside another remote-cache boundary during prerender. Each nested fill tries to open its own remote cache slot while the parent fill still holds the outer one. Prerender does not tolerate that stack.

Colour siblings are the other colourways of the same productNumber, shown as swatches or mini cards on the PDP and in carousels. When Centra's variant relation is thin, the card searches by productNumber instead of trusting relatedDisplayItems alone. That search lived in the same tagged helper as the PLP filter.

Split live from cached entry points

The shared fetch stays dumb. Tagged wrappers sit at the top level. Live variants skip the directive for callers already inside a remote boundary.

async function fetchCatalogProducts(variables) {
	const result = await centraFetchNoSession(ProductsDocument, { variables });
	return result.data.displayItems;
}
 
/** Callers already inside 'use cache: remote'. Nested remote fills timeout prerender. */
export async function filterProductsLive(variables) {
	return await fetchCatalogProducts(variables);
}
 
export async function filterProducts(variables) {
	'use cache: remote';
	const displayItems = await fetchCatalogProducts(variables);
	cacheTag(tags.products);
	// ...
}

lookupCatalogProductCached and lookupCatalogProductLive follow the same split. Colour-sibling search, content-page display-product hydrate, and reviews hydrate call the live variants. Route loaders and top-level sections keep the tagged surface.

I did not delete 'use cache: remote' from the codebase. I stopped nesting it. One remote boundary per prerender path. Hydrators inherit the parent's cache life instead of opening a second remote fill.

Document the rule for the next route

'use cache: remote' feels like the right default on every Centra touch until the caller already sits inside one. I added a short note in the catalog module: never nest 'use cache: remote' inside another remote cache during prerender. Code review now checks hydrate paths, not just page files.

Builds finish. Homepage prerender no longer hangs on remote fills that each think they own the clock. Editorial can publish a hero swap without me babysitting a deploy that used to die on minute six of a remote fill.

Remote cache is a boundary, not a blanket. Tag the edge, run plain fetches inside. When a hydrate helper needs Centra again, call the live entry point and let the parent boundary own the clock.


Happy coding! Sander