Cursors
find() doesn't return an array of documents — it returns a cursor, a pointer to the result set on the server that you iterate to pull documents in batches. Understanding cursors is essential for handling large result sets efficiently and safely.
Cursors Are Lazy
find() returns a cursor, not results
const cursor = db.products.find({ category: "widgets" })
// No documents have been fetched from the server yet — the query
// only actually runs once you start pulling results from the cursor.
cursor.hasNext() // triggers the first batch fetch
cursor.next()Batching
The server doesn't send every matching document in one response. It sends an initial batch (default 101 documents or 16 MB, whichever comes first), and the driver requests more batches ("getMore") as you keep iterating.
Controlling batch size
db.products.find().batchSize(50) // fetch 50 documents per network round-trip
Iterating: forEach() and toArray()
Two common iteration styles
// toArray() — materializes the ENTIRE cursor into memory as an array.
// Fine for small/bounded result sets; risky for huge ones.
const widgets = db.products.find({ category: "widgets" }).toArray()
// forEach() — streams documents one at a time without buffering them all
db.products.find({ category: "widgets" }).forEach((doc) => {
print(doc.name)
})toArray() on a cursor over an unbounded or very large result set can exhaust application memory. Prefer forEach(), an async iterator (in driver code), or add a limit() when you only need a bounded sample.Cursor Methods: limit, skip, sort, project
These methods are chainable on the cursor returned by find(). MongoDB applies them in a fixed logical order — sort, then skip, then limit — regardless of the order you write them in the chain.
Chaining cursor methods
db.products
.find({ category: "widgets" })
.sort({ price: -1 })
.skip(20)
.limit(10)
.project({ name: 1, price: 1 })Cursor Timeout
By default, a server-side cursor that sits idle for 10 minutes without any client activity is automatically closed. This protects the server from accumulating memory for cursors an application forgot about.
Setting | Behavior |
|---|---|
Default cursor | Server auto-closes it after ~10 minutes of inactivity |
noCursorTimeout: true | Disables the auto-close timeout — the cursor lives until explicitly closed or the client disconnects |
Disabling the timeout for a long-running scan
db.hugeCollection.find().noCursorTimeout()
noCursorTimeout() is a resource-leak risk if the client crashes or forgets to close the cursor — the server has no way to know it's abandoned. Always pair it with an explicit cursor.close() in a finally block, and avoid it unless a long-running batch job genuinely needs more than 10 minutes between reads.Cursor vs Aggregation
aggregate() also returns a cursor with the same iteration model as find(). The difference is expressive power: find() cursors support filtering, sorting, and simple projection; an aggregation pipeline can reshape, group, join, and compute — at the cost of being a bit more verbose for simple lookups.
Equivalent result, two APIs
// find() — simple filter + sort + limit
db.orders.find({ status: "shipped" }).sort({ total: -1 }).limit(5)
// aggregate() — same result, expressed as a pipeline
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $sort: { total: -1 } },
{ $limit: 5 }
])find() when a filter + sort + projection covers the need — it's simpler to read and, for a pure filter with no reshaping, performs identically to the equivalent aggregation pipeline. Reach for aggregate() the moment you need grouping, joins, or computed fields.Example Session
test> const cursor = db.products.find({ category: "widgets" }).sort({ price: 1 })
test> cursor.hasNext()
true
test> cursor.next()
{ _id: ObjectId('66a1...'), name: 'Small Widget', price: 4.99, category: 'widgets' }
test> cursor.next()
{ _id: ObjectId('66a2...'), name: 'Blue Widget', price: 9.99, category: 'widgets' }
test> cursor.itcount()
18find()returns a cursor — no query runs until you start iterating.Batching keeps large result sets from loading entirely into memory at once.
forEach()streams;toArray()buffers everything — pick based on result size.Cursors auto-close after 10 minutes idle unless
noCursorTimeout()is set (use with care).aggregate()returns a cursor too, with the same iteration model, but far more reshaping power.