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; usePromise.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
// 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: ~300msSequential 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
| Pattern | Total time | Dependencies | Typical use case |
|---|---|---|---|
| Sequential | Sum of all requests | Each depends on previous | Hierarchical data: user → posts → comments |
Parallel (Promise.all) | Max of all requests | None | Independent data: profile, analytics, notifications |
| Mixed | Optimized | Some dependencies | Most real apps |
| Streaming (Suspense) | Progressive | Flexible | Show 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
// 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
// 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)
// 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
// 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()
// 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
// 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
// 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
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 readyA concise answer to help you respond confidently on this topic during an interview.