Module P-1·26 min read

Sequential vs parallel fetching, unstable_cache and cache(), revalidatePath scope, connection() and unstable_noStore() for opting into dynamic rendering, and eliminating waterfall chains.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

P-1 — Advanced Data Fetching: Patterns, Caching, and Control

Who this is for: Developers who completed the Foundation phase and can build Next.js applications, but who need to move beyond the basics — eliminating waterfall fetches, using unstable_cache for persistent query caching, and understanding exactly which Next.js functions force a page into dynamic rendering. This is where "it works" becomes "it performs."


The Problem With Naive Data Fetching

Foundation-level data fetching — await db.query() in Server Components — is correct. But on a real application with multiple data sources, several failure modes emerge quickly:

Waterfall chains. Component A fetches user data, then renders Component B which fetches preferences, then renders Component C which fetches content. Three sequential round-trips when all three could have run in parallel.

No persistence across requests. React.cache() deduplicates within a single render, but the result is gone the moment the response is sent. Every request rebuilds the same data — a user's profile queried 10,000 times per minute means 10,000 database calls per minute, even when the profile hasn't changed.

Unintentional dynamic rendering. You introduce a cookie check in one small utility function and suddenly a page that was statically generated at build time is now server-rendering on every request. The build output flips from to λ and your CDN cache hit rate collapses.

This module addresses all three.


Sequential vs Parallel — Recognising the Waterfall

The waterfall is the most common performance issue in App Router applications. It happens whenever you await multiple independent fetches sequentially:

tsx

These three queries don't depend on each other — getPreferences doesn't need the result of getUser. They should run in parallel:

tsx

Spotting the pattern: any time you see multiple await statements at the top level of a Server Component or data function, ask: does the second await need the result of the first? If not, they can be parallelised.

Promise.allSettled for Graceful Degradation

Promise.all rejects if any promise rejects — one failed query takes down the whole fetch. For non-critical data, use Promise.allSettled:

tsx

The profile still renders if activity fails to load. Use Promise.allSettled for supplementary data, Promise.all for essential data where partial failure means the page can't render meaningfully.


Suspense as a Parallelism Tool

Beyond the loading skeleton use-case from F-4, Suspense has a second role: it allows independent subtrees to fetch in parallel without blocking each other, regardless of how deep the component tree goes.

tsx

Each Suspense boundary is independent — StatsPanel fetching doesn't block ActivityFeed from starting. The fastest one renders first; slow ones stream in later. The page never waits for the slowest query before showing anything.

Compare this to a single page-level await for all three data sources: the page would be blank until the slowest one resolves.

The mental model: each <Suspense> boundary is its own independent loading state. Design your component tree so slow data is isolated in its own boundaries, not mixed with fast data in the same await.


unstable_cache — Persisting Query Results Across Requests

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.