Data: Fetching & Mutating

Lesson 1 of 5

Fetching in Server Components

"fetch requests are not cached by default and will block the page from rendering until the request is complete." — Next.js Documentation, "Fetching Data"

If you have used Next.js before version 15, read that sentence again, because it reverses something you almost certainly believe. For several years fetch in the App Router was cached by default — a decision that was well-intentioned, widely misunderstood, and the source of an entire genre of bug report titled some variant of "my data is stale and I do not know why". It is not the default any more. A fetch in a server component now goes to the network on every request unless you say otherwise.

That change makes this chapter's material simpler to reason about and easier to get wrong in a new way. Simpler, because a request now does what it appears to do. Wrong in a new way, because "goes to the network every time" combined with "blocks the page from rendering until complete" means an unremarkable-looking component can hold your entire page hostage to somebody else's API.

Part II established that a server component can query a database directly and that its code and credentials never reach the browser. This chapter is about doing that well: where to put a fetch, why you do not have to thread it through props, what the framework deduplicates for you and what it does not, and — the part that separates fast pages from slow ones — how to notice that you have written a waterfall.

Almost everything here reduces to one question asked repeatedly: does this request need to wait for that one? Where the answer is no and your code says yes, you have a waterfall. Where the answer is yes, you have a dependency, and the tool is not to eliminate it but to stop it blocking everything else.

The big question

If every component fetches exactly what it needs, with no props threaded and no coordination, how does the page avoid making the same request five times — and how do you tell that apart from five requests that merely happen in a bad order?

Fetch where the data is used

The first instinct carried over from client-side React is to fetch high in the tree and pass the results down, because fetching in two places meant two requests. In a server render that instinct is unnecessary and mildly harmful.

Identical fetch requests are memoized during a server render pass. A GET with the same URL and the same options, called from five different components, executes once and shares the result — across server components, layouts, pages, generateStaticParams and generateViewport. So the component that needs the data can simply ask for it.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

That trailing-slash row is not a contrived example; it is one of the two ways this silently stops working in practice. The other is a header or option added in one call site and not the others — an authorisation header, a next.tags array, a signal — which makes two calls that look identical in review into two distinct requests.

If you need to defeat memoization deliberately, the documented mechanism is to pass an AbortController signal, which makes the request unique.

Memoization only covers fetch. For an ORM query or a file read there is no URL to key on, and the equivalent tool is React's cache() from the previous part — wrap the function once and every call site shares the result.

Everyday analogy

Imagine an office where several people each need today's exchange rate. The old arrangement was that one person looked it up in the morning and wrote it on a whiteboard, and everybody else read the board — which worked, and meant that adding a new team required someone to remember to tell them where the board was, and that a team who needed a different rate had to negotiate for board space. The new arrangement is that anyone who needs a rate simply asks, and a clerk on the door notices when two people ask the same question within the same hour and answers the second from memory. Nobody coordinates. Nobody drills a value through three layers of management. The catch is that the clerk matches questions exactly: "the euro rate" and "the euro rate, please" are two questions, and the clerk will go and look both up. That is the trailing slash, and the added header, and the cache-busting timestamp — small differences that a person would consider identical and a matching key does not.

Not cached, and blocking

Now the behaviour that has changed. A fetch in a server component is not cached by default and blocks rendering until it completes. Both halves matter, and the second is the one that shows up in your latency graphs.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

That final distinction is worth holding onto, because both options make a page feel faster and they are answers to different questions. Caching removes the request. Streaming keeps the request and stops it blocking. Part IV is about the first; the fourth chapter of this part is about the second.

There is a development-only wrinkle that has confused a great many people, and it is documented: Next.js keeps an HMR cache for fetch responses in local development, and it applies even to cache: 'no-store', so an uncached request may not show fresh data between hot reloads. It clears on navigation or a full reload. If you are staring at a value that refuses to change while you edit, reload the page before you doubt your code.

A little history

The App Router shipped in Next.js 13 with fetch cached by default and force-cache as the implicit mode, and the reasoning was defensible: most page data changes rarely, caching by default makes the common case fast, and opting out is one option away. What happened instead is one of the better-documented lessons in framework defaults. Developers wrote a fetch, saw correct data, deployed, and discovered days later that the page had been frozen since the build — because nothing in the code said "cached", the caching was invisible in review, and the failure mode was not an error but a stale number. The default was reversed in Next.js 15, and the reversal is worth studying rather than just noting. A cached-by-default system fails silently and late: wrong data, no signal, discovered by a user. An uncached-by-default system fails loudly and early: a slow page, visible immediately, discovered by you. Neither default is correct in the abstract, and the second is better in practice because of where the failure lands. That is a principle worth carrying beyond this framework — when choosing a default, ask not which is right more often, but which is wrong more usefully.

The waterfall you did not mean to write

Here is the mistake that costs the most and looks the most innocent. Two awaits in a row are sequential, even when neither depends on the other.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

Three hundred and ninety milliseconds against one hundred and eighty, for a change that moves two await keywords. And the sequential version is what you get by writing the obvious thing, which is why this is worth checking for deliberately rather than expecting to notice.

The tell in review is a run of const x = await … lines at the top of a component. Some of those may genuinely depend on each other. Most do not.

The engineer's notebook

I read the top of every server component the same way now: I look at the run of awaits and ask, for each one, whether the line above it produced anything the line uses. If not, they should have been started together, and the fix is mechanical enough that I make it without discussion. The second habit is subtler and took me longer to acquire — I have stopped trying to remove dependencies that are real. There is a reflex, once you learn about waterfalls, to treat every sequential request as a defect and to go looking for a way to flatten it, and sometimes that means inventing a combined endpoint or denormalising something that did not want denormalising. If the second request genuinely needs an id from the first, that is not a waterfall, it is a dependency, and the correct response is to stop it blocking the rest of the page rather than to pretend it is not there. The third thing I check is the one nobody thinks to: how long the FIRST request takes. In a dependent chain everything else is waiting on it, so a slow root is the only latency in the chain that is worth optimising, and it is the one people skip past on their way to the interesting part.

When sequential is genuinely required

Some requests really do depend on earlier ones — you cannot fetch an artist's playlists before you know the artist's id. That is a dependency, not a mistake, and the tool is different.

The problem is not that the second request happens late. It is that everything else on the page is waiting behind both. Wrapping the dependent part in <Suspense> lets the rest render immediately and the chain stream in when it resolves.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

That last observation joins this chapter to Part I. loading.tsx wraps the page in a Suspense boundary automatically, so the layout renders instantly and the whole page streams. Finer control comes from placing <Suspense> yourself, closer to the slow thing — and the documentation now recommends exactly that, because a boundary near the uncached access is more precise than one at the segment.

There is a caveat carried over from Part I worth restating in this context: a layout that performs uncached or runtime data access does not fall back to a loading.tsx in its own segment, because loading sits below layout in the hierarchy. It blocks the navigation instead. Wrap the slow part of the layout in its own boundary, or move the fetch into the page.

In the real world

A team investigating a product page that took most of two seconds to render found six awaits in a row at the top of the component, which is exactly the shape this chapter warns about, and their first change was to convert all six to Promise.all. It made almost no difference. Five of the six were fast; a single call to a pricing service accounted for nearly all of the time, and running the other five alongside it saved about eighty milliseconds out of nineteen hundred. The change that actually worked was to accept that the pricing call was slow, wrap the price in a Suspense boundary, and let the rest of the page render without it — the page went from two seconds of nothing to two hundred milliseconds of a complete product page with a shimmer where the number goes. What I find worth retelling is the order the team tried things in, because it was the natural one and it was backwards: they applied the technique they had just learned, and only afterwards measured which request was slow. Parallelising six calls of unknown duration is guesswork; the measurement takes a minute and tells you that five of them were never the problem. The general form is that latency work should start with a distribution, not a pattern.

Getting data to client components

The last piece. A client component cannot query a database, so the data has to come from a server component — and there are three ways to send it, suited to different situations.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

The absent await is genuinely the crux, and it is invisible in review — a diff that adds one is a diff that turns a streaming page into a blocking one, with no other change and no warning.

For data driven by interaction rather than by rendering — a search box, an infinite list, a polling widget — a client-side data library like SWR or React Query remains entirely appropriate. That is the legitimate case from the previous part, and the boundary between it and the anti-pattern is the same test: is this data needed to render, or because something happened.

Common Mistakes

  1. Assuming fetch is still cached by default. It is not, since Next.js 15. Every request goes to the network unless you cache it deliberately.

  2. Forgetting that an uncached fetch blocks rendering. A slow third-party call holds the whole page until it returns, however small the component that made it.

  3. Consecutive awaits for independent requests. A request starts when the function is called, not when it is awaited. Call them all, then Promise.all.

  4. Breaking memoization by accident. A trailing slash, a cache-busting parameter, or a header added at one call site and not another makes two requests out of what looks like one.

  5. Expecting memoization to apply in a Route Handler. It does not — route handlers are not part of the React component tree.

  6. Expecting memoization to cover an ORM call. It only applies to fetch. Wrap other data functions in React's cache().

  7. Reaching for caching to fix a slow personal request. Caching removes the request; only streaming keeps it and stops it blocking. Per-user data is a streaming problem.

  8. Adding an await in front of a promise you meant to pass down. The Suspense fallback then never shows and the page blocks. The missing keyword is the feature.

  9. Putting an uncached fetch in a layout and expecting loading.tsx to cover it. loading sits below layout, so the navigation blocks instead. Wrap it in its own boundary or move it to the page.

  10. Debugging stale data in development without reloading. The HMR cache applies even to cache: 'no-store' and clears on navigation or a full reload.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
How do you fetch data in a Server Component, and do you need to pass it down to avoid fetching twice?
Model answer — You make the component async and await the call directly — either fetch or a database query, since the code never reaches the browser. You do not need to drill it down, because identical fetch requests are memoized during a render pass: the same GET with the same options called from five components runs once. For an ORM call there is no URL to key on, so you wrap the function in React's cache() to get the same effect.
Mid
A page makes four independent requests and takes as long as all four combined. What is wrong and how do you fix it?
Model answer — They are written as consecutive awaits, so each starts only after the previous resolves. A request begins when you call the function, not when you await it, so the fix is to call all four first and then await them together with Promise.all — the page then takes as long as the slowest one. Before doing that, though, I would check the durations, because if one call dominates, parallelising saves very little and the real fix is to wrap that one in a Suspense boundary so it streams instead of blocking. Parallelising helps when the calls are comparable; streaming helps when one is an outlier.
Senior
Your team upgraded and pages that were fast are now slow, with no code changes. What happened?
Model answer — Most likely the fetch caching default reversed — it used to be cached by default and is not any more, so requests that were previously served from the build now hit the network on every render, and because an uncached fetch blocks rendering, the slowest upstream call is now in every page's critical path. The fix is not to restore the old behaviour globally but to decide per call site, which is what the change was for: data that is the same for everyone and tolerates staleness gets cached explicitly; data that is personal or must be fresh gets a Suspense boundary so it streams rather than blocks. I would find the affected routes by looking at which are still prerendered at build time versus which have gone dynamic. The wider point is that the old default failed silently with stale data and the new one fails visibly with latency, which is the better trade even though the migration is painful — and the migration is exactly this audit.
Company lens — how the same topic gets asked
micro1Practical: build a page that fetches from two endpoints and renders both. They are watching whether you write consecutive awaits or start them together.
GoogleMechanism: what exactly is memoized, over what scope, and keyed on what? Expect follow-ups on where it does not apply and why.
MetaPerceived performance: one request is slow and the rest are fast. Describe what the user sees under each of the possible arrangements.
AmazonLatency ownership: how do you stop a third-party dependency's slow day from becoming your page's slow day?
Hands-on

Two functions covering the two things that decide a page's data latency. The first collapses a list of requests by memo key, so you can see how many network calls a render actually makes and which near-identical calls failed to match. The second is the waterfall analysis: given a dependency graph of requests, compute how long the page takes when everything independent runs in parallel, and which chain is the critical path. Watch the case where a request depends on two others, and the one where a duplicate appears inside a chain.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

Summary

  • fetch is not cached by default in current Next.js, and an uncached request blocks rendering until it completes.

  • Identical fetch requests are memoized within a render pass, so fetch in the component that needs the data rather than drilling it down.

  • The memo key is method, URL and options. A trailing slash, a query parameter or a header added at one call site makes a second request.

  • Memoization is not a cache — it collapses duplicates within one request — and it does not apply in route handlers or to non-fetch calls. Use React's cache() for those.

  • A request starts when you call the function, not when you await it, which is the whole of the parallelisation technique.

  • Consecutive awaits are sequential even for independent data. Call them all, then Promise.all.

  • Layouts and pages render in parallel by default; the waterfall is something you create inside a single component.

  • A genuine dependency is not a waterfall. Do not flatten it — wrap it in <Suspense> so it stops blocking the rest of the page.

  • Caching and streaming answer different questions. Caching removes the request; streaming keeps it and stops it blocking. Personal data is a streaming problem.

  • To get data into a client component, pass the unresolved promise and read it with use(). Adding an await in the server component silently undoes the streaming.

Reading is only half of it. The next chapter turns to writing — how a form submission or a button click runs code on the server without an API route, what a Server Action actually is on the wire, and why the ergonomics of calling a function hide a public endpoint that has to defend itself. The next chapter is Server Actions.

Editorial reflection

The reversal of the fetch caching default is the most instructive thing in this chapter, and not because caching is interesting. It is instructive because it is a rare case of a framework publicly concluding that a default which optimised the common case was wrong, and the reasoning was not about correctness — both defaults are correct, they just fail differently. Cached-by-default fails as stale data: no error, no signal, discovered by a customer looking at yesterday's price. Uncached-by-default fails as latency: visible on the first page load, discovered by whoever wrote it. The second is enormously better despite being the same amount of wrong, because of who finds out and when. I think this generalises further than most people apply it. When we choose defaults — in a library, a config file, a schema, a permission model — the instinct is to ask which value is right more often, and that is the easier question. The more useful one is what happens when the default is wrong: does it produce something loud and local, or something quiet and distant? A great deal of what we call good engineering judgement turns out, on inspection, to be a preference for failures that arrive early and name themselves.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.