MongoDB search and listing streams in chunks: cursor, batchSize, and getMore
In MongoDB, large search/list responses are not returned all at once: the server sends cursor batches, and your API can forward data progressively in HTTP chunks.
Executive summary
In MongoDB, large search/list responses are not returned all at once: the server sends cursor batches, and your API can forward data progressively in HTTP chunks.
Last updated: 2/27/2026
Executive summary
The key point that often gets mixed up is:
- MongoDB does not return large queries in one full payload.
- It opens a cursor and returns an initial
firstBatch. - As the client consumes results, the driver requests additional batches via
getMore.
So "streaming" for list/search endpoints is usually a two-layer pipeline:
- batched streaming between database and application (cursor + getMore);
- chunked streaming between application and HTTP client (NDJSON/SSE/chunked transfer).
Once this model is clear, designing high-volume listing APIs becomes much more predictable.
1) How MongoDB actually returns query results
find() and aggregate() return cursors. According to MongoDB docs:
- default initial batch is the lesser of
101documents or16 MiB; - subsequent batches are also bounded by
16 MiB; batchSizecan lower batch volume per round trip.
Operational flow:
- API sends
find; - MongoDB returns
firstBatch+cursorId; - once
firstBatchis consumed, driver issuesgetMore; - repeat until cursor is exhausted.
So the database already operates in chunks before you decide your HTTP response strategy.
2) Your API decides how to expose the stream
Application-side options:
toArray()-> load all matching docs into memory, respond at the end;- cursor iteration (
for await) -> progressive response; cursor.stream()-> Node Readable stream integration.
For high-volume listings, iteration/streaming is typically safer because first bytes can be sent earlier and memory pressure stays lower.
3) Practical example: order export as chunked listing
Endpoint: /orders/export
- Collection:
orders - Filter: period + status
- Sort:
_idascending - Output: NDJSON streaming
tsimport { once } from 'node:events';
app.get('/orders/export', async (req, res) => {
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
res.setHeader('Transfer-Encoding', 'chunked');
const cursor = db.collection('orders')
.find({ status: 'paid' })
.sort({ _id: 1 })
.batchSize(500);
try {
for await (const doc of cursor) {
const ok = res.write(JSON.stringify(doc) + '\n');
if (!ok) {
await once(res, 'drain'); // HTTP socket backpressure
}
}
res.end();
} finally {
await cursor.close();
}
});What happens under the hood:
- the driver does not fetch one document per DB round trip;
- it fetches internal batches and emits documents progressively;
- your API can emit one doc (or grouped docs) as HTTP chunks.
4) Common mistakes
Mistake A: using toArray() everywhere
Fine for small volumes. Risky for large volumes due to memory spikes and poor time-to-first-byte.
Mistake B: extreme batchSize
- too small: too many
getMoreround trips; - too large: higher memory and per-batch latency.
batchSize should be tuned with real payload and latency data.
Mistake C: assuming chunked HTTP fixes slow queries
If filter/sort are not index-friendly, DB execution remains the bottleneck. Streaming improves delivery behavior, not query planning.
5) Relation to event streaming (Kafka/Flink)
To avoid concept overlap:
- search/list streaming: on-demand read via cursor batches;
- event streaming: continuous event flow.
MongoDB participates in both:
- cursors for query/list endpoints;
- Change Streams for CDC/event pipelines.
Both are progressive processing patterns, but with different contracts.
Practical checklist for MongoDB listing endpoints
- Avoid
toArray()on high-volume routes. - Use
for awaitorcursor.stream(). - Tune
batchSizewith latency/memory measurements. - Implement HTTP backpressure handling.
- Close cursors explicitly on cancellation/errors.
- Track TTFB, throughput, and memory per endpoint.
Conclusion
If you want the precise explanation for MongoDB search/list streaming, it is:
- MongoDB returns query results in cursor batches;
- the driver fetches follow-up batches via **
getMore**; - your API can expose that progressively as HTTP chunks.
Practical review question: does your highest-volume listing endpoint stream as data arrives, or does it still wait to load everything into memory first?
Sources
- cursor.batchSize() (MongoDB Docs) - official documentation
- getMore command (MongoDB Docs) - official documentation
- Cursors and Cursor Batches (MongoDB Docs) - official documentation
- Access Data From a Cursor - Node.js Driver (MongoDB Docs) - official documentation
- MongoDB Change Streams (MongoDB Docs) - official documentation