Skip to content

Server data fetching without waterfalls

An await holds only what renders below it: start independent reads together, share repeats with cache(), and stream the slow part.

Predict first

Before you read: predict

ExamHall's attempt page has a layout, the page, and a <Timer> inside the page. Each prints the signed-in candidate's name, and each calls getCandidate(), defined once in lib/session.js as cache(async () => db.candidates.find(sid)), where sid comes from the session cookie. The page renders on every request. Ana opens the page, then Ben opens it.

How many queries does the candidates table receive in total?

Pick one

The 30-second version

In a Server Component each await holds whatever that component renders, so reads awaited in turn, or placed in a child that renders only after its parent's read, start late. Start independent reads together. React's cache() lets repeated calls share one call per request. A slow read awaited inside <Suspense> streams instead of holding the page.

An await holds only what renders below it: start independent reads together, share repeats with cache(), and stream the slow part.

How it works

Step 1 of 11

Server reads cost round trips. Each awaited call waits for its reply; reads awaited in turn add up before the page is sent.
In the kitchen
  • asking for the reference only after the home-visit report is back → reads awaited in turn (steps 1, 5)

Analogy: A cat rescue centre preparing one adoption

For each adoption the centre needs three replies from outside: the home-visit volunteer's report, a character reference for the adopter and the cat's history file from its previous owner. A coordinator who asks for the reference only after the home-visit report is back, though the reference needs nothing from it, makes the adopter wait for both in turn. The cattery team and the adoption office start on an adoption at the same moment. The volunteer told to pack the bedding only once the photographer is done starts late. The adopter's ID is checked once per adoption and written in that adoption's folder, and everyone working on it reads it there; the next adoption opens a new folder. The adopter takes the cat home with a slip, "history file: to follow".

At the rescue centre In Next
asking for the reference only after the home-visit report is back reads awaited in turn (steps 1, 5)
the cattery team and the adoption office starting together sibling components, a layout and its page (step 3)
the volunteer waiting for the photographer a child under an awaiting parent (step 4)
the ID check written in the adoption folder cache() within one request (step 6)
a new folder for the next adoption a new cache per request (step 7)
the cat now, "history file: to follow" a slow part streamed behind <Suspense> (step 11)

In real terms: an await holds its component and every child below it; reads that need nothing from each other belong side by side or started together, and repeats share one call per request.

Breaks down when: a folder stays in the cabinet after the adoption, while React drops its cache when the request ends. Wrong inference: "the next request finds the lookup already done."

Contrasting case: a Client Component that fetches in an effect starts its read only after the page has reached the browser and hydrated, a wait no server-side fix removes (the browser cache: fsd-server-state-caching).

Examples, run for real

4.1 ExamHall: the attempt page as found

EXH-3340: the attempt page awaits the candidate (from the session cookie), the exam, the candidate's attempt and the saved answers, one after another, and prints them. The data server holds each read until the script answers, in rounds: each round it replies to every read it holds.

4.1 /attempt as found: reads held per round: round 1 candidate · round 2 exam · round 3 attempt · round 4 answers
4.1 reads in flight at once, at most 1 · first byte after 4 rounds of replies

Each read started only after the previous reply, so four rounds of replies came before the candidate's browser got a byte. The exam needs nothing from the candidate, yet it waited (steps 1, 5).

4.2 Proof domain, payments ledger: one merchant lookup per request

A merchant's statement page has two cards, the balance and the payouts, side by side. Each calls getMerchant(), which reads the session cookie and queries the merchants table.

4.2 plain getMerchant(): merchants queries for one view 2
4.2 wrapped in cache(): one view 1 · a second merchant's view 1 more (2 in all) · each page shows its own merchant yes

As a plain function, each card queried. Wrapped in cache(), the two cards shared one query, and a second merchant's visit queried again and got their own name (steps 6, 7).

4.3 Proof domain, payments ledger: the slow part streams

The statement page prints its heading, then <Suspense fallback={<p>Loading pending payouts…</p>}><PendingPayouts /></Suspense>, and PendingPayouts awaits the slow pending-payouts read. The script holds that read, looks at what has arrived, then releases it.

4.3 pending payouts held: received heading yes · "Loading pending payouts…" yes · payouts no · response ended no
4.3 released: payouts received in the same response yes · status 200

The heading and the fallback reached the browser while the read was still out; the payouts followed in the same response (step 11).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

The attempt page renders two components side by side; it awaits nothing itself:

The database holds every read and has replied to none yet.

Which reads has it not received yet?

Code for this question
export default function AttemptPage() {  return <main><ExamHeader /><AttemptPanel /></main>;}async function ExamHeader() { const exam = await db.exams.find(); return <div><h1>{exam}</h1><Rules /></div>; }async function Rules() { const rules = await db.rules.find(); return <p>{rules}</p>; }async function AttemptPanel() { const candidate = await db.candidates.find(); return <div><p>{candidate}</p><AttemptStatus candidate={candidate} /></div>; }async function AttemptStatus({ candidate }) { const attempt = await db.attempts.find(candidate); return <p>{attempt}</p>; }
Pick one

Question 2 of 7

A merchant's statement page shows two cards side by side, both rendered per request:

The merchants table logs two lookups for every view of the page.

Which change makes it one?

Code for this question
// lib/merchant.jsexport const getMerchant = cache(async (key) => db.merchants.find(key.id));// the statement pageasync function BalanceCard() { const m = await getMerchant({ id: MERCHANT }); return <Balance m={m} />; }async function PayoutList() { const m = await getMerchant({ id: MERCHANT }); return <Payouts m={m} />; }
Pick one

Question 3 of 7

The attempt page, rendered per request:

The answers' read is slow. Today the candidate sees the heading and "Loading answers…" at once, then the answers.

Which one change makes the browser receive nothing at all until the answers' read returns?

Code for this question
export default function AttemptPage() {  return <main>    <h1>ECON-201 attempt</h1>    <Suspense fallback={<p>Loading answers…</p>}><Answers /></Suspense>  </main>;}async function Answers() { const a = await getAnswers(); return <p>Saved answers: {a}</p>; }
Pick one

Question 4 of 7

The statement route, rendered per request. The layout awaits getMerchant() and wraps the page. The page:

None of the four reads needs another's result. The database answers in rounds: each round it replies to every read it holds at that moment.

How many rounds of replies come before the merchant's browser receives its first byte?

Code for this question
export default async function StatementPage() {  const balance = await getBalance();  const payouts = await getPayouts();  return <main>    <p>{balance}</p><p>{payouts}</p>    <Suspense fallback={<p>Loading disputes…</p>}><Disputes /></Suspense>  </main>;}async function Disputes() { const d = await getDisputes(); return <p>{d}</p>; }
Pick one

Question 5 of 7

MR !3640 rewrites EXH-3340's attempt page. The first chunk must show the candidate's name and the exam title, and the same response must still bring the attempt and the saved answers.

Write the page so that the candidate's browser gets its first byte after at most 2 rounds of replies.

Question 6 of 7

The statement route's layout and its page each run the same three reads, and none is wrapped in cache():

The page renders per request. A merchant opens it once.

How many requests reach the rates API and the fees API, and how many queries reach the merchants table?

Code for this question
const rate = await (await fetch(RATES_URL)).text();                                   // a GET to the exchange-rate APIconst fee = await (await fetch(FEES_URL, { method: 'POST', body: 'm-4471' })).text(); // a POST to the fees APIconst merchant = await db.merchants.find('m-4471');
Pick one

Question 7 of 7

The exam header, rendered per request; getExam and getRules are database reads:

The rules need only the exam id.

Which change starts the rules read while the exam read is still out, and still sends one rules query per visit?

Code for this question
async function ExamHeader({ examId }) {  const exam = await getExam(examId);  return <div><h1>{exam}</h1><Rules examId={examId} /></div>;}async function Rules({ examId }) { const rules = await getRules(examId); return <p>{rules}</p>; }
Pick one

Go deeper

Pro tips
  1. To find a waterfall, hold every read and look at which ones have started. A data layer that logs each call as it starts, behind a switch that delays every reply, shows in one page view which reads wait for others; a read that starts only after an unrelated reply is the waterfall. When: a page is slow to send its first byte (steps 2–4). Verify (RAN, Node 24.21.0: lib/data-server.mjs and lib/observe.mjs hold and log every read for this lesson's scripts).
  2. Remember that fetch sharing stops at route handlers. Next memoizes fetch only inside the React component tree; a route handler that calls the same URL twice makes two requests. When: moving page logic into an API route (step 9). Status: DOC (Next.js docs, fetch: "Memoization does not apply in Route Handlers, since they are not part of the React component tree"); not run.
Source
  • Next.js docs, Fetching Data: https://nextjs.org/docs/app/getting-started/fetching-data · next-fetching-data.txt. Named in the curriculum row. "By default, layouts and pages are rendered in parallel"; "within any component, multiple async/await requests can still be sequential if placed after the other"; Promise.all; "For data access that does not use fetch, such as an ORM or database query, wrap the function in React.cache"; "React.cache is scoped to the current request only. Each request gets its own memoization scope with no sharing between requests"; preloading ("call the data-fetching function without await before blocking work … The data-fetching function must deduplicate matching calls"); streaming with <Suspense>. Supports steps 1–7, 9–11. Valid as of 2026-10-09.
  • React docs, cache: https://react.dev/reference/react/cache · react-cache.txt. Named in the curriculum row. "React will invalidate the cache for all memoized functions for each server request"; "Each call to cache creates a new function … that do not share the same cache"; "Call cache outside of any components"; "React will call Object.is on the input to verify if there is a cache hit"; the preload pattern. Supports steps 6–8, 10.
  • Next.js docs, fetch: https://nextjs.org/docs/app/api-reference/functions/fetch · next-fetch.txt. "fetch requests using GET with the same URL and options are automatically memoized during a server render pass"; "Memoization does not apply in Route Handlers". Supports step 9 and pro tip 2.
  • Next.js docs, Streaming: https://nextjs.org/docs/app/guides/streaming · next-streaming.txt. Background for step 11 (streaming order itself is next-rendering-modes-ppr's).
  • Next.js docs, Route segment config and Caching without Cache Components: next-route-segment-config.txt, next-caching-previous-model.txt. dynamic = 'force-dynamic', as the fixtures use it; "If you are not using fetch (which is automatically memoized) … wrap your data access with the React cache function". Background for the fixture and step 9.

Checked against the tools and docs as of 2026-10-09.