The Math of the Disaster: Firestore N+1 Queries and the Visibility Gap
N+1 query patterns are expensive in any database. In Firestore, every extra document read is billed, so a “clean” component tree that fetches one author per card can quietly turn into a cost disaster. This post breaks down the visibility gap between local testing and production, then shows two practical fixes: denormalization and batched in queries. For how reads are counted in general, see how Firestore read costs work.
The visibility gap in action
- On localhost: You have three posts. That is four reads (one list plus three authors). You do not notice a thing.
- In production: A user scrolls through 50 posts. That is 51 reads.
- At scale: 1,000 users doing this daily equals about 1.5 million reads per month for that single list pattern.
The gap exists because small datasets hide quadratic or linear-multiplier behavior. Production traffic and larger lists expose it immediately on your invoice.
Visualizing the invisible
This is where a tool like ReadMeter helps. Instead of deploying and hoping, ReadMeter can flag something like loadAuthor() running 50 times right in your development environment.
You might write code that feels clean, but when you watch the dev widget, you can discover a waterfall of reads that would cost hundreds of dollars at scale. That is the same class of problem as being careful with Firestore queries—only now the issue is per-item fetches layered on top of a list query.
Two ways to fix N+1 Firestore queries
When you see a read spike, the fix usually means changing how you model data or how you query it.
Method 1: Data flattening (the NoSQL way)
In Firestore, reads are expensive, but storage is relatively cheap. The straightforward way to drop reads from 51 to 1 for the list is to denormalize author display fields onto each post.
When creating a post, save the author’s display data inside the post document:
await addDoc(collection(db, "posts"), {
title: "Optimizing Firestore in Next.js",
content: "...",
// Flattened author data
author: {
id: user.uid,
name: user.displayName,
avatarUrl: user.photoURL,
},
createdAt: serverTimestamp(),
});
Read impact after flattening
Your PostList component only needs to fetch the posts collection. One list fetch is one query and N document reads for N posts—no per-row author round trips.
Tradeoff: if profiles change often, you may need to update many post documents or accept slightly stale embedded author fields.
Method 2: Batched fetching with in queries
If user profiles change often, fetch all posts, collect unique author IDs, then load those users in as few queries as allowed by Firestore’s in limits (split into chunks of 10 if you exceed the limit).
import {
collection,
getDocs,
query,
where,
documentId,
limit,
} from "firebase/firestore";
import { db } from "@/lib/firebase";
export default async function PostList() {
// 1. Fetch all posts
const postsSnapshot = await getDocs(
query(collection(db, "posts"), limit(20))
);
const posts = postsSnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
// 2. Extract unique author IDs
const authorIds = [...new Set(posts.map((post) => post.authorId))];
// 3. Batch fetch authors
const authorsMap: Record<string, object> = {};
if (authorIds.length > 0) {
const authorsQuery = query(
collection(db, "users"),
where(documentId(), "in", authorIds)
);
const authorsSnapshot = await getDocs(authorsQuery);
authorsSnapshot.docs.forEach((doc) => {
authorsMap[doc.id] = doc.data();
});
}
return (
<div>
{posts.map((post) => (
<PostCard
key={post.id}
post={post}
author={authorsMap[post.authorId]}
/>
))}
</div>
);
}
Read impact after batching
The page load above uses two queries (posts + authors), which removes the N+1 waterfall as long as child components do not fetch authors again.
If authorIds.length can exceed 10, split authorIds into chunks of 10 and run one in query per chunk (still far fewer than one query per post).
The ReadMeter shortcut: AI-assisted refactoring
Manually rewriting these queries takes time. With ReadMeter, you can use Copy Fix Prompt on the offending function to get a context-rich prompt tailored to your code.
Paste it into Cursor, Claude, or ChatGPT—for example:
Refactor
loadAuthor()inPostCard.tsx. ReadMeter detected it executed 20 times and triggered an N+1 pattern. Rewrite the parent component to batch author lookups using a Firestoreinquery and pass the data down as props.
When the model sees real read counts and where the pattern fires, it can produce a batched query that matches your codebase on the first try.
Bottom line: Treat list + per-item fetches as a first-class cost risk. Flatten when reads matter more than write fan-out; batch with in when you need canonical user documents. Measure in development so production never surprises you.
See your Firestore reads while you build
Get visibility into read patterns before traffic arrives. One-time purchase, one device.
Get ReadMeter — $9 (one-time)