NextjsParallel & Sequential Fetching

Parallel & Sequential Fetching

When a Server Component needs data from more than one source, how you write the awaits determines whether those requests happen one after another or all at once. Getting this wrong is one of the most common performance mistakes in App Router apps — it is easy to write requests sequentially by accident and end up with a page that is far slower than it needs to be.

Sequential fetching (the request waterfall)

If one piece of data depends on the result of another, sequential fetching is required — you have no choice. But if the two requests are independent, awaiting them one at a time still forces the second request to wait for the first to finish, even though nothing about it actually depends on that result.

TSX
async function getUser(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}`)
  return res.json()
}

async function getRecommendations() {
  const res = await fetch('https://api.example.com/recommendations')
  return res.json()
}

export default async function ProfilePage({ params }: { params: { userId: string } }) {
  // getRecommendations() does not depend on the user at all,
  // but this still waits for getUser() to fully finish first.
  const user = await getUser(params.userId)
  const recommendations = await getRecommendations()

  return (
    <>
      <h1>{user.name}</h1>
      <RecommendationList items={recommendations} />
    </>
  )
}
Warning
If getUser takes 300ms and getRecommendations takes 400ms, the sequential version above takes roughly 700ms total — the two delays stack up, even though there is no real dependency between them.
Parallel fetching with Promise.all

When requests are independent, kick them both off first, then await them together with Promise.all. The underlying network requests happen concurrently, and the total wait time is roughly the duration of the slowest request, not the sum of both.

TSX
export default async function ProfilePage({ params }: { params: { userId: string } }) {
  // Both requests start immediately, without waiting on each other
  const [user, recommendations] = await Promise.all([
    getUser(params.userId),
    getRecommendations(),
  ])

  return (
    <>
      <h1>{user.name}</h1>
      <RecommendationList items={recommendations} />
    </>
  )
}

With the same 300ms and 400ms requests, the parallel version finishes in roughly 400ms — the slower of the two — instead of 700ms.

Genuinely sequential (dependent) data

Some data really does depend on a previous result, and there is no way around awaiting it in order. That is fine — the goal is not to eliminate all sequential awaits, only the accidental ones.

TSX
export default async function OrderPage({ params }: { params: { orderId: string } }) {
  // shippingAddress genuinely depends on the order's customerId,
  // so this sequence is unavoidable and correct.
  const order = await getOrder(params.orderId)
  const shippingAddress = await getAddress(order.customerId)

  return <OrderSummary order={order} address={shippingAddress} />
}
Parallel vs. sequential at a glance

Pattern

Total time (roughly)

Use when

Sequential awaits

Sum of every request

Request B genuinely needs a value from request A

Promise.all([...])

Duration of the slowest request

Requests are independent of each other

Streaming in parallel with Suspense

For an even better perceived experience, independent data-fetching sections of a page can each be wrapped in their own Suspense boundary and rendered by a separate async component. Next.js streams each section in as soon as its own data is ready, instead of waiting for the slowest one to block the entire page.

TSX
import { Suspense } from 'react'

export default function DashboardPage() {
  return (
    <>
      <Suspense fallback={<p>Loading profile…</p>}>
        <Profile />
      </Suspense>
      <Suspense fallback={<p>Loading recommendations…</p>}>
        <Recommendations />
      </Suspense>
    </>
  )
}
Tip
When reviewing a Server Component, scan for consecutive await calls on functions that don't reference each other's results. That pattern is almost always an avoidable waterfall — a good candidate for Promise.all or separate Suspense boundaries.