Study guide

7.3 Cursor Batching, Streaming & Memory Handling

MongoDB Associate Developer Exam

Track your progress as you read

Finished sections get a check mark.

1 Chapter 1: Document Model & MongoDB Architecture

1.1 Document Model Fundamentals & BSON Data Types 1.2 Databases, Collections & Schema Flexibility 1.3 Distributed Architecture Fundamentals: Replica Sets & Sharded Clusters

2 Chapter 2: CRUD Operations: Create & Read

2.1 Document Insertion Operations 2.2 Query Selectors & Filter Operators 2.3 Array Queries & Element Evaluation 2.4 Projections, Cursor Methods & Collation

3 Chapter 3: CRUD Operations: Update & Delete

3.1 Field Update Operators 3.2 Array Update Operators & Positional Modifiers 3.3 Document Replacement & Deletion Operations 3.4 Atomic Find-and-Modify Operations

4 Chapter 4: Aggregation Framework Mastery

4.1 Pipeline Fundamentals & Core Stages ($match, $project, $addFields) 4.2 Grouping, Reshaping & Array Manipulation ($group, $unwind, $replaceWith) 4.3 Multi-Document Joins & Faceted Pipelines ($lookup, $facet, $bucket) 4.4 Materialization & Window Operations ($out, $merge, $setWindowFields) 4.5 Atlas Search: Search Index Definitions & $search Queries

5 Chapter 5: Indexing Strategies & Performance Optimization

5.1 Index Types & Storage Mechanics 5.2 Compound Indexes & the Equality-Sort-Range (ESR) Rule 5.3 Multikey Indexes & Array Indexing Constraints 5.4 Specialized Index Types: TTL, Partial, Unique & Text 5.5 Query Optimization & Explain Plan Analysis

6 Chapter 6: Data Modeling & Schema Design Patterns

6.1 Embedding vs. Referencing Tradeoffs 6.2 Core Schema Design Patterns 6.3 Schema Validation with JSON Schema

7 Chapter 7: Application Development & Driver Integration

7.1 Connection Strings & Connection Pool Management 7.2 Driver CRUD Paradigms & Object Mapping 7.3 Cursor Batching, Streaming & Memory Handling 7.4 Write Concerns, Read Concerns & Retryable Operations

8 Chapter 8: Developer Tooling & Administration Essentials

8.1 mongosh Scripting & Administrative Commands 8.2 Data Ingestion & Export Utilities 8.3 Backup, Restore & Performance Telemetry 8.4 Atlas Sample Dataset & Data Explorer

7.3 Cursor Batching, Streaming & Memory Handling

Key Takeaways Explain This Topic AI

MongoDB query execution is lazy: 'collection.find()' creates a client-side cursor pointer without transmitting documents until iteration begins.

The server returns an initial batch limited to 101 documents or 1 MB of BSON data, followed by subsequent batches of up to 16 MiB retrieved via 'getMore' commands.

The 'batchSize(n)' cursor method allows developers to tune document counts per batch, balancing network round-trips against application heap memory.

Server cursors time out after 10 minutes of inactivity; 'noCursorTimeout: true' disables this but requires explicit 'cursor.close()' in a finally block to prevent server memory leaks.

Invoking 'cursor.toArray()' buffers the entire result set into application RAM, risking heap out-of-memory crashes; streaming or async iteration ('for await') must be used for large datasets.

Last updated: September 2026

7.3 Cursor Batching, Streaming & Memory Handling

When an application queries MongoDB for hundreds of thousands or millions of documents, transferring the entire dataset in a single monolithic payload would overwhelm network buffers, saturate client memory, and cause catastrophic application crashes. To solve this problem, MongoDB implements Cursors , Network Batching Protocols , and Server-Side Cursor Management .

A clear understanding of how cursors work, how the getMore wire protocol operates, and how to stream large datasets without triggering memory exhaustion is a core competency tested on the MongoDB Associate Developer Exam.

1. Cursor Architecture & Lazy Evaluation

A Cursor is not an in-memory array of documents. Rather, it is an active pointer to a result set residing on the database server ( mongod or mongos ).

Lazy Evaluation Mechanics

When a developer executes a find() query in an application driver: // In Node.js / PyMongo / Java:

const cursor = db.collection("orders").find({ status: "COMPLETED" });

At the moment this line executes, no database query has been sent over the wire , and no documents have been transferred . The driver simply instantiates a local cursor object with the query specifications. Execution is deferred until the application attempts to read the first document (e.g., via await cursor.next() , for await (const doc of cursor) , or cursor.stream() ). +---------------------------------------------------------------------------------------------------+

  • | MongoDB Cursor Batching Protocol |
  • | |
  • | 1. INITIAL QUERY COMMAND |
  • | Client ====================== find({ status: "COMPLETED" }) ======================> Server |
  • | Client <================ { cursor: { id: 8941029412L, firstBatch: [...] } } <======= Server |
  • | * First Batch Limit: 101 documents OR 1 MB of BSON data (whichever limit is hit first). |
  • | |
  • | 2. SUBSEQUENT BATCH RETRIEVAL (getMore) |
  • | Client ============== getMore: 8941029412L, collection: "orders" ==================> Server |
  • | Client <==================== { cursor: { nextBatch: [...] } } <====================== Server |
  • | * Subsequent Batch Limit: Up to 16 MiB of BSON data (or configured batchSize). |
  • | |
  • | 3. CURSOR CLOSURE |
  • | Client ============== getMore: 8941029412L, collection: "orders" ==================> Server |
  • | Client <==================== { cursor: { id: 0L, nextBatch: [...] } } <============== Server |
  • | * Server returns cursorId = 0, indicating all documents drained; server frees cursor state. |
  • +---------------------------------------------------------------------------------------------------+
  • The Batching Protocol: Initial Batch vs. getMore

MongoDB divides query results into discrete batches transmitted over the network:

The Initial Batch ( find command) :

The server evaluates the query and returns the First Batch ( firstBatch ).

Batch Threshold : Contains a maximum of 101 documents or 1 Megabyte (1 MB) of BSON data, whichever limit is reached first.

If the entire query result set fits within this initial limit, the server returns cursorId: 0 immediately, closing the cursor in a single network round-trip.

If more matching documents remain, the server assigns a non-zero 64-bit integer cursorId and pins the cursor state in server RAM.

Subsequent Batches ( getMore command) :

When the client application iterates past the last document in its local buffer, the driver transparently sends a getMore command with the cursorId .

Batch Threshold : Subsequent batches contain up to 16 mebibytes (16 MiB) of BSON data — the BSON maximum message size — or the limit specified by batchSize() . batchSize() can only lower this ceiling, never raise it.

This process repeats until the query is exhausted.

Cursor Termination ( cursorId: 0 ) :

Once the last document has been transmitted, the server returns cursorId: 0 , and the database engine immediately releases all server-side memory, locks, and resources allocated to the cursor.

  • 2. Cursor Configuration & Timeout Rules
  • Tuning Batch Size with batchSize()

Developers can override the default batch size using the .batchSize() cursor method: // Override batch size to 500 documents per network round-trip

  • const cursor = db.collection("telemetry")
  • .find({ device_type: "SENSOR_V2" })
  • .batchSize(500);

Architectural Tradeoffs of batchSize() :

Smaller Batch Size (e.g., batchSize(20) ) : Reduces memory consumption on the client and server. Useful when documents are large (several megabytes each) or when processing each document is slow, reducing wasted data transfer if the consumer aborts early.

Larger Batch Size (e.g., batchSize(1000) ) : Maximizes network throughput by minimizing the number of getMore round-trips when processing millions of small documents in bulk ETL pipelines.

The 10-Minute Cursor Idle Timeout

By default, the MongoDB server automatically closes active cursors that have been idle for more than 10 minutes (600,000 ms) .

Why Idle Timeouts Occur:

If an application retrieves a batch of 100 documents and performs slow, blocking processing on each item (e.g., sending an email or calling a third-party HTTP API for 10 seconds per item), processing the batch takes 100 * 10 = 1,000 seconds (~16.6 minutes) . When the driver finally attempts to fetch the next batch with getMore , the server has already deleted the idle cursor, throwing a CursorNotFound exception (Error Code 43) .

Disabling Timeouts with noCursorTimeout: true

For long-running batch jobs, developers can disable the 10-minute timeout: // Node.js: Disabling cursor timeout

  • const cursor = db.collection("large_dataset")
  • .find({}, { noCursorTimeout: true })
  • .batchSize(50);

CRITICAL PRODUCTION WARNING (Memory Leak Risk): When noCursorTimeout: true is set, the MongoDB server will never automatically close the cursor . If the client application crashes, encounters an unhandled exception, or exits the loop early without draining all documents, the cursor remains permanently open on mongod , consuming server RAM and locks until the database process is restarted.

Mandatory Rule: When using noCursorTimeout: true , you MUST wrap iteration in a try...finally block and explicitly invoke cursor.close() in the finally clause. // Correct Pattern for noCursorTimeout in Node.js

const cursor = db.collection("large_dataset").find({}, { noCursorTimeout: true });

  • try {
  • for await (const doc of cursor) {
  • await processHeavyDocument(doc);
  • }
  • } finally {

// Guarantees cursor is closed on mongod even if processing throws an error!

  • await cursor.close();
  • } # Correct Pattern for no_cursor_timeout in Python (PyMongo)
  • cursor = db.large_dataset.find({}, no_cursor_timeout=True).batch_size(100)

try:

for doc in cursor:

process_heavy_task(doc)

finally:

cursor.close() # Mandatory cleanup

3. Iteration Paradigms & The toArray() Memory Trap

How an application iterates over a cursor fundamentally impacts memory consumption and garbage collection behavior.

The toArray() Anti-Pattern // DANGEROUS ANTI-PATTERN: toArray() on large result sets

const allOrders = await db.collection("orders").find({}).toArray(); // 1,000,000 documents!

res.json(allOrders);

Why toArray() Causes Production Outages:

toArray() forces the driver to immediately fetch every single matching document across multiple getMore requests and allocate all documents into a single contiguous JavaScript array in heap memory.

If the query matches 500,000 documents of 2 KB each, toArray() attempts to allocate over 1 GB of heap memory simultaneously. In Node.js (which defaults to a 2–4 GB heap limit), this immediately triggers a FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory crash.

In Python, this causes high memory bloat and MemoryError ; in Java, it triggers java.lang.OutOfMemoryError: Java heap space .

Recommended Iteration Patterns

Iteration Method Execution Behavior Memory Profile Recommended Use Case

toArray() Fetches and buffers all documents into an in-memory array High ( O ( N ) O(N) O ( N ) memory proportional to total result set) Small, strictly bounded queries ( N ≤ 100 N \le 100 N ≤ 100 )

Async Iterator ( for await ) Pulls documents one-by-one from local batch; fetches next batch on demand Minimal ( O ( 1 ) O(1) O ( 1 ) memory; only current batch in RAM) Standard record-by-record business processing

Readable Stream ( .stream() ) Emits data events with full backpressure handling Minimal ( O ( 1 ) O(1) O ( 1 ) constant memory) Exporting large datasets to CSV, S3, or HTTP responses

forEach() cursor method Iterates over documents sequentially using callback Minimal ( O ( 1 ) O(1) O ( 1 ) constant memory) Functional processing without building in-memory lists

Stream-Based Processing Example (Node.js)

Using Node.js streams allows piping millions of documents from MongoDB directly to an HTTP response or disk file with a near-zero memory footprint: import { pipeline } from "stream/promises";

import { Transform } from "stream";

  • app.get("/api/export-orders", async (req, res) => {
  • const cursor = db.collection("orders")
  • .find({ year: 2026 })
  • .batchSize(1000);
  • // Transform BSON document stream to JSON lines
  • const jsonLineTransformer = new Transform({
  • objectMode: true,
  • transform(doc, encoding, callback) {
  • callback(null, JSON.stringify(doc) + "\n");
  • }
  • });
  • res.setHeader("Content-Type", "application/x-ndjson");
  • res.setHeader("Content-Disposition", 'attachment; filename="orders.ndjson"');
  • try {
  • // Handles backpressure automatically: pauses cursor when client network is slow
  • await pipeline(cursor.stream(), jsonLineTransformer, res);
  • } catch (err) {
  • console.error("Streaming pipeline failed:", err);
  • }
  • });

Loading diagram... Cursor Batching & getMore Sequence Flow