Knowledge

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.

2/27/202610 min readKnowledge
MongoDB search and listing streams in chunks: cursor, batchSize, and getMore

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:

  1. batched streaming between database and application (cursor + getMore);
  2. 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 101 documents or 16 MiB;
  • subsequent batches are also bounded by 16 MiB;
  • batchSize can lower batch volume per round trip.

Operational flow:

  1. API sends find;
  2. MongoDB returns firstBatch + cursorId;
  3. once firstBatch is consumed, driver issues getMore;
  4. 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: _id ascending
  • 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 getMore round 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

  1. Avoid toArray() on high-volume routes.
  2. Use for await or cursor.stream().
  3. Tune batchSize with latency/memory measurements.
  4. Implement HTTP backpressure handling.
  5. Close cursors explicitly on cancellation/errors.
  6. 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

Related reading