Skip to main content

Parallel and sequential data fetching in Next.js

Parallel data fetching starts all requests at once and waits for the slowest one to finish; sequential fetching waits for each request to complete before starting the next.

Theory

TL;DR

  • Sequential = waterfall: 200ms + 300ms + 200ms = 700ms total
  • Parallel = Promise.all(): max(200ms, 300ms, 200ms) = 300ms total
  • Use parallel when requests are independent; use sequential when later data depends on earlier results
  • Promise.all() rejects if any request fails; use Promise.allSettled() when partial failures are acceptable
  • Server Components in Next.js do not parallelize requests automatically - you have to structure the code yourself

Quick example

tsx
// Sequential: all durations add up const user = await getUser(id); // 200ms const posts = await getPosts(id); // 300ms (starts after user finishes) const comments = await getComments(id); // 200ms (starts after posts finish) // Total: ~700ms // Parallel: only the slowest request matters const [user, posts, comments] = await Promise.all([ getUser(id), // 200ms getPosts(id), // 300ms getComments(id), // 200ms - all three start at the same time ]); // Total: ~300ms

Sequential adds every duration. Parallel takes only the longest. On a page with 5-6 independent requests, that difference becomes very visible to users.

Key difference

Sequential fetching creates a dependency chain where each await blocks the entire render pipeline until that request resolves. Parallel fetching creates all promises immediately and waits for the last one to settle. The time savings compound at scale: three 300ms requests take 900ms sequentially but only 300ms in parallel. Network latency is usually the bottleneck in server-side rendering, so this matters a lot.

When to use

  • Parallel when requests are independent (user profile, posts, followers - none need each other's data)
  • Sequential when later data depends on earlier results (fetch user first, then fetch the user's team using user.teamId)
  • Mixed when some requests are independent and some are not (parallelize everything that shares the same ID, then fetch dependent data sequentially)
  • Streaming with Suspense when you want to show partial UI while slower data loads in the background

Comparison table

PatternTotal timeDependenciesTypical use case
SequentialSum of all requestsEach depends on previousHierarchical data: user → posts → comments
Parallel (Promise.all)Max of all requestsNoneIndependent data: profile, analytics, notifications
MixedOptimizedSome dependenciesMost real apps
Streaming (Suspense)ProgressiveFlexibleShow UI incrementally

How it works internally

When you call Promise.all([...]), JavaScript creates all promises immediately and they start executing right away. Then the async function suspends until the last one settles. The event loop stays free for other work. In Next.js Server Components, this all happens during server render, and the client receives fully rendered HTML.

One thing I notice regularly: developers add Promise.all() and assume they are done. But if there is even one await before calling Promise.all(), a sequential bottleneck already exists. The code looks parallel but is not.

Common mistakes

Mistake 1: Waterfall hidden inside what looks like parallel code

tsx
// Wrong: reviews still waits for product to resolve const product = await getProduct(productId); const [reviews, recommendations] = await Promise.all([ getReviews(product.id), // waits for product first getRecommendations(productId) ]); // Correct: start all promises before awaiting anything const productPromise = getProduct(productId); const reviewsPromise = getReviews(productId); const recommendationsPromise = getRecommendations(productId); const [product, reviews, recommendations] = await Promise.all([ productPromise, reviewsPromise, recommendationsPromise ]);

Mistake 2: Promise.all() fails on any rejection

tsx
// Wrong: one failure loses user and posts too const [user, posts, comments] = await Promise.all([ getUser(id), getPosts(id), getComments(id) // one failure crashes everything ]); // Correct: allSettled() for non-critical data const results = await Promise.allSettled([ getUser(id), getPosts(id), getComments(id) ]); const user = results[0].status === 'fulfilled' ? results[0].value : null; const posts = results[1].status === 'fulfilled' ? results[1].value : []; const comments = results[2].status === 'fulfilled' ? results[2].value : [];

Mistake 3: Sequential fetching inside loops (N+1 problem)

tsx
// Wrong: fetches one user at a time const userIds = [1, 2, 3, 4, 5]; const users = []; for (const id of userIds) { const user = await getUser(id); // 5 * 200ms = 1000ms users.push(user); } // Correct: all five requests fire at once const users = await Promise.all( userIds.map(id => getUser(id)) // ~200ms total );

Mistake 4: Thinking Server Components parallelize automatically

tsx
// Wrong: still sequential even in a Server Component export default async function Page() { const user = await getUser(id); const posts = await getPosts(id); // waits for user for no reason return <div>{/* ... */}</div>; } // Correct: explicitly parallel export default async function Page() { const [user, posts] = await Promise.all([ getUser(id), getPosts(id) ]); return <div>{/* ... */}</div>; }

Next.js does not reorder your await calls. Parallelism is your responsibility.

Mistake 5: Passing already-resolved values to Promise.all()

tsx
// Wrong: each request already ran sequentially before Promise.all const user = await getUser(id); const posts = await getPosts(id); return Promise.all([user, posts]); // these are values, not promises // Correct: pass the promises themselves return Promise.all([getUser(id), getPosts(id)]);

Real-world usage

  • Next.js dashboard pages: fetch user profile, analytics, and notifications in parallel before rendering
  • React Query: useQueries() runs multiple queries in parallel; useQuery() runs one at a time
  • GraphQL: single HTTP request where multiple fields resolve in parallel on the server
  • Express middleware: Promise.all([checkAuth(), checkRateLimit(), logRequest()]) runs checks together
  • Database queries: Promise.all([db.users.find(), db.posts.find()]) instead of sequential calls

Follow-up questions

Q: When would you use Promise.allSettled() instead of Promise.all()?
A: When one failure should not block everything else. Promise.all() rejects immediately if any promise rejects. allSettled() waits for all of them and returns each result with a status. Use it for non-critical data like recommendations or analytics, where a fetch failure should show a fallback rather than crash the page.

Q: Can there be a waterfall inside a parallel fetch?
A: Yes. await Promise.all([getUser(id), getPosts(user.id)]) - the second request still needs user to resolve before it can start. The fix: start all promises before Promise.all(), or restructure so getPosts() takes a shared ID that does not depend on a prior fetch.

Q: How does Suspense streaming change the decision between parallel and sequential?
A: With Suspense you can show partial UI while slow data loads separately. Fetch critical data like user profile in parallel with everything else, then stream non-critical parts like recommendations behind a Suspense boundary. You get the speed of parallel with the UX of progressive rendering.

Q: You have 10 independent API calls and one consistently takes 3 seconds. How do you optimize without changing the API?
A: Run all 10 in parallel. Show the 9 fast results through Suspense boundaries while the slow one streams in. Alternatively, use Promise.allSettled() with a timeout wrapper to show a fallback after a threshold. The goal is to not let one slow request block the rest of the page.

Q: How do you verify that your fetches are actually parallel?
A: Open DevTools Network tab and look at the timeline. Parallel requests overlap; sequential ones start only after the previous one ends. On the server, add console.time() / console.timeEnd() around Promise.all() and compare the total time to the sum of individual requests.

Examples

Basic: sequential vs parallel in a Server Component

tsx
// Sequential: posts waits for user even though it does not need its data export default async function ProfilePage({ userId }: { userId: string }) { const user = await getUser(userId); // 200ms const posts = await getUserPosts(userId); // 300ms - could have started earlier // Total: ~500ms return <div><h1>{user.name}</h1><PostList posts={posts} /></div>; } // Parallel: both start at the same time export default async function ProfilePage({ userId }: { userId: string }) { const [user, posts] = await Promise.all([ getUser(userId), // 200ms getUserPosts(userId), // 300ms - starts immediately ]); // Total: ~300ms return <div><h1>{user.name}</h1><PostList posts={posts} /></div>; }

Both fetch the same data. The parallel version is 200ms faster. Add more independent requests and the gap keeps growing.

Intermediate: dashboard with three independent data sources

tsx
// Next.js Server Component - all three requests start at the same time export default async function Dashboard({ userId }: { userId: string }) { const [user, analytics, notifications] = await Promise.all([ db.user.findUnique({ where: { id: userId } }), db.analytics.getUserStats(userId), db.notifications.getUnread(userId) ]); // None of these depend on each other - Promise.all makes that explicit return ( <div> <UserCard user={user} /> <AnalyticsChart data={analytics} /> <NotificationBell count={notifications.length} /> </div> ); }

None of these requests need each other's data. There is no reason to wait for user before fetching analytics. Promise.all() makes the intent clear: all three are independent.

Advanced: mixed pattern with dependent and independent requests

tsx
export default async function ProductPage({ productId }: { productId: string }) { // Start all three promises immediately. // reviews and recommendations only need productId, which we already have. const productPromise = getProduct(productId); const reviewsPromise = getReviews(productId); const recommendationsPromise = getRecommendations(productId); const [product, reviews, recommendations] = await Promise.all([ productPromise, reviewsPromise, recommendationsPromise ]); // Seller data needs product.sellerId - sequential here is intentional const seller = await getSeller(product.sellerId); return ( <div> <ProductCard product={product} seller={seller} /> <ReviewList reviews={reviews} /> <RecommendationGrid items={recommendations} /> </div> ); }

Starting promises before Promise.all() is the key pattern. If the first line had been const product = await getProduct(productId), reviews and recommendations would have waited an extra 300ms for no reason. The sequential fetch for seller at the end is intentional and correct - it genuinely needs product.sellerId.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?