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 Korf3 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, and review strips hydrate from Centra GraphQL and Sanity. I tagged the 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.

One remote boundary was not enough

The homepage loader already ran under 'use cache: remote'. Good. Then hydration fanned out: colour siblings on a card sometimes need a Centra productNumber search when variant relations are incomplete, 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 is 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

The trap keeps coming back because 'use cache: remote' feels like the right default on every Centra touch. It is, 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 a stack of remote fills that each think they own the clock.

Remote cache is a boundary, not a blanket. Tag the edge, run plain fetches inside.


Happy coding! Sander