A query can match more documents than an application should hold in one array. A cursor lets the application consume results incrementally while the driver or shell retrieves batches. That makes large jobs possible without assuming every matching document fits in memory, but the cursor is stateful and owns resources while it remains open. This MongoDB 8.0 reference uses mongosh's cursor methods in complete fixtures. The same lifecycle principles apply to the Node driver, whose async iteration and stream interfaces have their own documented details. The examples reset only named collections in a disposable fixture database.
Distinguish query configuration from consumption
find returns a cursor rather than an already materialized list of all matches. Add the intended projection, sort, limit, and batch configuration before consuming results. The query's supported index and filters determine how the server finds and orders matches; cursor iteration does not repair an inefficient query plan. A sort should include a unique tie-breaker when stable order matters to the application or a test. The fixtures sort by numeric _id to make their expected sequence explicit.
next advances the cursor and returns one document, while hasNext checks whether more results are available under the API's behavior. A cursor can fetch additional batches during those operations. Consuming one document changes its state. A later toArray collects remaining documents, not a magic fresh copy of every original match. Keep that distinction visible when a debug helper reads one item before the real processing function. Passing an already consumed cursor between components without documenting its state can silently omit earlier records.
Use batches without confusing them with a result limit
batchSize influences how many documents are requested in batches, subject to the server's response-size and protocol rules. It is not a cap on the total number of matching documents. A cursor with batchSize two can still return all five documents in the fixture. limit controls how many results the query returns overall. Small batches can increase round trips; large batches can increase client buffering. Choose a reasonable value for the payload shape and processing speed rather than assuming one setting is optimal for every job.
Application memory depends on what the consumer does as well. Iterating one document at a time then pushing every document into an array still accumulates the full result. For an export, stream or write each processed result under an appropriate output policy. For a numerical summary, retain only the bounded accumulator when the calculation permits it. Server-side aggregation may be more efficient than transferring all rows for a simple total, but incremental iteration remains useful when each document requires application-specific work.
Give the cursor one consumption owner
Do not launch concurrent next or other state-changing operations on the same cursor. Its position is shared mutable state, so competing consumers cannot safely assume which document each receives. Also avoid mixing iteration methods casually. A Node driver cursor offers async iteration, toArray, and streaming-related interfaces, but using several at once can create confusing ownership. Choose one consumer and a clear transfer point if ownership changes.
The main mongosh fixture uses a sequential loop and a finally cleanup. A Node program can use for await with its driver cursor and still explicitly manage early exit according to the documented API. Process each result or enqueue it into a bounded worker system; unbounded parallel tasks merely move the memory problem from the cursor to pending promises. If work fails halfway through a long migration, record a suitable restart checkpoint or design idempotent processing. A cursor is a retrieval handle, not a durable job scheduler or a guarantee that earlier application side effects were rolled back.
Close early and define consistency expectations
Exhausting a finite cursor releases its server-side retrieval state through normal behavior, but an application that stops early should close its owned cursor. Use finally so success, processing errors, and deliberate early exit all reach cleanup. Connection and session shutdown are additional lifecycle decisions; closing one cursor does not necessarily close the whole client. A long-running cursor can encounter timeouts or session-related limits, so disabling one timeout is not a promise of indefinite survival.
A cursor should not be described as a timeless snapshot of a changing collection unless the actual read concern and transaction context provide the required semantics. Documents may change while a long job runs. Define whether the job needs snapshot consistency, a bounded query cutoff, or an eventual result. Rewinding or recreating a query can observe changed data. For stable resumable processing, combine a documented ordering key, filters, and checkpoint policy rather than relying on skip counts alone. The small static fixtures intentionally avoid concurrent writers so their output tests consumption behavior directly.
MongoDB documentation and version notes
MongoDB Cursor Iteration and Cleanup Examples
Each example shows the code, the output it produces and what each part does.
Summing a projected cursor in small batches
Run the complete script in mongosh on the isolated MongoDB 8.0 fixture. Five documents contain amounts one through five. The cursor requests small batches and a projection, then a sequential loop sums the amounts while retaining only the identifiers needed for this tiny verification. Its batch size does not truncate the five results. The finally block closes the cursor whether the loop completes or throws. The stated total is fifteen. For a real large export, avoid retaining the identifier array; it is present here solely to show that every controlled result was consumed in the declared sort order.
const rows = db.getSiblingDB('codevra_ref_cursor').getCollection('rows');
rows.deleteMany({});
rows.insertMany([1, 2, 3, 4, 5].map(n => ({ _id: n, amount: n, note: 'fixture' })));
const cursor = rows.find({}, { _id: 1, amount: 1 }).sort({ _id: 1 }).batchSize(2);
let total = 0;
const ids = [];
try {
while (cursor.hasNext()) {
const row = cursor.next();
total += row.amount;
ids.push(row._id);
}
} finally { cursor.close(); }
print(JSON.stringify(ids));
print(total);[1,2,3,4,5]
15MongoDB Cursor Iteration and Cleanup in Real Projects
Mix next and toArray on one cursor
Read the first result with next, then materialize only the remaining results with toArray. Use a new demo collection so the practice runs independently. Print the first identifier and the identifiers returned afterward. The solution closes the cursor in finally even though the finite result is consumed. This exercise is useful for recognizing a subtle debugging mistake: a function that inspects the first document has advanced the cursor passed to another function. If both functions need the complete data, create separate queries under the intended consistency policy or explicitly include the already read document. Do not assume toArray resets the position.
const rows = db.getSiblingDB('codevra_ref_cursor').getCollection('practice');
rows.deleteMany({});
rows.insertMany([1, 2, 3, 4, 5].map(n => ({ _id: n })));
const cursor = rows.find().sort({ _id: 1 }).batchSize(2);
try {
const first = cursor.next();
const remaining = cursor.toArray();
print(first._id);
print(JSON.stringify(remaining.map(row => row._id)));
} finally { cursor.close(); }1
[2,3,4,5]MongoDB Cursor Iteration and Cleanup FAQ
Does batchSize two return only two documents?
No. It configures batch retrieval, subject to server limits, while the cursor can continue fetching until the result is exhausted. Use limit for an overall result cap. The main fixture retrieves five documents with a batch size of two. Choose batch size by considering document size, round-trip cost, and processing speed. It is not an authorization or resource-limit substitute for the whole job.
Why can toArray omit the first document?
An earlier next call already consumed it. toArray materializes the remaining cursor results at its current position. Keep consumption state explicit when passing a cursor between functions. A diagnostic read is not harmless if it advances the same cursor the application later uses. Store the consumed document intentionally or create a separate query if both consumers require independent complete results.
Does incremental iteration guarantee low memory use?
Only if the consumer also keeps its work bounded. Pushing every row into an array or launching an unlimited promise per row can still exhaust memory. Retain a bounded accumulator, stream output, or use a controlled work queue. Batch retrieval reduces one source of eager materialization; it does not decide the application's retention policy. Measure realistic document sizes and concurrency when selecting limits.
Should I close a cursor when stopping early?
Yes, when your component owns it. Use cleanup that also runs after errors and deliberate early exit. Closing the cursor is distinct from closing the client connection and ending a session. A long-running job also needs a restart and consistency policy. Do not assume an open cursor survives forever or that recreating it later yields the identical snapshot of a collection that has changed.
MongoDB Cursor Iteration and Cleanup Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
After the worked cursor reads _id 1 with next, what does toArray return?
MongoDB Cursor Iteration and Cleanup Quiz
2 questions to check what you learned.
Why avoid concurrent next calls on one cursor?
- They compete over one shared consumption position
- MongoDB requires every cursor to return only one document
- Concurrent reads automatically create independent snapshots
What must accompany incremental retrieval for bounded memory?
- A bounded retention and processing policy in the application
- An array that stores every document forever
- An unlimited promise for every matched document