findOne vs find: Cursors Explained
Learners will retrieve documents using findOne and iterate a find cursor, understanding how MongoDB streams large result sets.
findOne vs find: Cursors Explained is a free MongoDB Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the MongoDB Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Two Ways to Read Documents
MongoDB provides two primary methods for reading documents from a collection:
- findOne(filter, projection) — retrieves the first document matching the filter and returns it as a plain document object (or
nullif nothing matches) - find(filter, projection) — retrieves all matching documents and returns a cursor, a lazy iterator that streams results from the server one batch at a time
Understanding when to use each method—and how cursors work—is fundamental to writing efficient MongoDB queries.
findOne: Simple and Direct
findOne() is the simplest way to retrieve a single document. It returns the first document that matches the filter, or null if no document matches. If multiple documents match, MongoDB returns whichever it encounters first in its internal order—add a .sort() before calling this if you need a specific one.
Common use cases for findOne: looking up a user by email, fetching a product by SKU, or checking if a record exists. Because it returns a plain object rather than a cursor, you use the result directly without iteration.
// findOne by _id (most common lookup)
const user = await db.collection('users').findOne(
{ _id: ObjectId('64a2f3b1...') }
);
if (!user) {
throw new Error('User not found');
}
console.log(user.name); // 'Alice'
// findOne with a filter
const admin = await db.collection('users').findOne({ role: 'admin' });
// Returns ONE admin doc (undefined order), or nullWhat Is a Cursor?
A cursor is a pointer to the result set of a query. When you call find(), MongoDB does not immediately transfer all matching documents to the client. Instead it opens a server-side cursor and sends documents in batches (default 101 documents per batch). The client fetches the next batch only when the current batch is exhausted.
This design is crucial for memory efficiency. If a query matches 10 million documents and you loaded them all at once, you would crash the client. With a cursor, you process documents one batch at a time, keeping memory usage constant regardless of result set size.
// find() returns a cursor, not documents
const cursor = db.collection('orders').find({ status: 'pending' });
// No data fetched yet!
// Data flows as you iterate:
for await (const order of cursor) {
// Each iteration fetches from server in batches
console.log(order._id);
}
// Cursor is exhausted — server releases itIterating Cursors in Node.js
Node.js driver cursors support multiple iteration patterns. The most modern approach is for await...of (async iteration), which cleanly handles backpressure and error handling. Alternatives include cursor.toArray() which loads all results into memory—convenient but dangerous for large result sets.
Always close cursors when done if you break out of iteration early (e.g., after finding what you need). An open cursor holds resources on the MongoDB server. Use cursor.close() explicitly, or rely on for await...of which auto-closes the cursor on loop completion or error.
// Pattern 1: async for...of (recommended)
const cursor = db.collection('products').find({ inStock: true });
for await (const product of cursor) {
await processProduct(product);
}
// Pattern 2: toArray() - loads all into memory
const products = await db.collection('products')
.find({ inStock: true }).toArray();
// Pattern 3: forEach
await cursor.forEach(product => console.log(product.name));Cursor Batch Size and getMore
Internally, the cursor protocol works in two phases:
- The initial
findcommand returns the first batch (default 101 documents or 16 MB, whichever comes first) - Each subsequent batch is fetched via a
getMorecommand using the cursor ID
You can customize the batch size with cursor.batchSize(n). A smaller batch size reduces memory usage on both sides but requires more network round-trips. A larger batch size is more efficient for large sequential scans. The default is usually optimal—only tune it for specific workloads.
// Set a custom batch size (rarely needed)
const cursor = db.collection('logs')
.find({})
.batchSize(500);
// Count documents in a cursor without loading them
// (MongoDB 4.4+ supports .count() on cursor for backwards compat)
// Prefer countDocuments() for accurate counts:
const count = await db.collection('logs').countDocuments({});
console.log('Total logs:', count);Cursor Timeout and Sessions
By default, MongoDB cursors time out after 10 minutes of inactivity on the server side. If your processing of each batch takes longer than that, the cursor will be killed and you will get a CursorNotFound error when you try to fetch the next batch.
For long-running operations, set noCursorTimeout: true or use a session to keep the cursor alive. However, be aware that noCursorTimeout holds a server cursor open indefinitely—always explicitly close these cursors when done to avoid resource leaks.
// Long-running cursor that won't time out
const cursor = db.collection('bigCollection').find(
{},
{ noCursorTimeout: true }
);
try {
for await (const doc of cursor) {
await slowProcessing(doc); // Takes > 10 minutes total
}
} finally {
// Always close explicitly when using noCursorTimeout
await cursor.close();
}Chaining Modifiers on find()
The cursor returned by find() supports a fluent API—you chain methods to modify the query before iteration begins. Order matters for readability but not execution (MongoDB sends all modifiers together):
.sort({ field: 1 })— sort direction.limit(n)— max documents.skip(n)— skip first n results.projection({ field: 1 })— select fields.hint({ index: 1 })— force a specific index.maxTimeMS(ms)— abort if query takes too long
// Full chained query: filter → sort → skip → limit → projection
const page2Products = db.collection('products').find(
{ category: 'Electronics', inStock: true },
{ name: 1, price: 1, _id: 0 } // projection as 2nd arg
)
.sort({ price: -1 }) // Descending price
.skip(20) // Skip page 1 (20 items)
.limit(20) // Page size 20
.maxTimeMS(5000); // Abort if > 5sTailable Cursors for Capped Collections
A special cursor type called a tailable cursor works only on capped collections. Unlike normal cursors that close when all results are consumed, a tailable cursor blocks and waits for new documents, similar to Unix's tail -f command on a log file.
Tailable cursors were the original mechanism for real-time data streaming in MongoDB before Change Streams were introduced. They are still useful for lightweight log tailing on capped collections where Change Streams are overkill.
// Tailable cursor on a capped collection
const tailCursor = db.collection('appLogs').find(
{},
{ tailable: true, awaitData: true }
);
// Blocks and awaits new log entries indefinitely
for await (const log of tailCursor) {
console.log('[' + log.level + '] ' + log.message);
// Prints each new log as it is inserted
}findOne vs find: Choosing Correctly
Use this rule of thumb for choosing between findOne and find:
- Use findOne when: you expect exactly one result (lookup by unique key), you only need to check existence, or you want the simplest code for a single-record API endpoint
- Use find when: the query may return zero, one, or many results; you are building a list endpoint; you need cursor control (batchSize, maxTimeMS); or you are processing results without loading all into memory
Avoid find({}).toArray() on large collections—it loads all results into memory. Process with for await...of instead.
// GOOD: findOne for unique key lookup
const user = await db.collection('users').findOne({ email: 'alice@test.com' });
// GOOD: find with streaming for large sets
for await (const doc of db.collection('users').find({ active: true })) {
await sendNewsletter(doc);
}
// BAD: loading millions of docs into memory
const allUsers = await db.collection('users').find({}).toArray();
// Could OOM crash your server!The explain() Method on Cursors
Appending .explain('executionStats') to a cursor shows how MongoDB executes the query instead of returning documents. The output reveals:
winningPlan.stage:IXSCAN(uses an index) orCOLLSCAN(full scan—bad for large collections)nReturned: how many documents were returnedtotalDocsExamined: how many documents MongoDB looked at to find results (should be close to nReturned if an index is used)executionTimeMillis: total execution time
Regularly running explain() on your critical queries is the foundation of MongoDB performance tuning.
// Check query execution plan
const plan = await db.collection('users')
.find({ email: 'alice@test.com' })
.explain('executionStats');
console.log(plan.queryPlanner.winningPlan.stage);
// 'IXSCAN' if email is indexed, 'COLLSCAN' if not
console.log(plan.executionStats.nReturned); // 1
console.log(plan.executionStats.totalDocsExamined); // 1 (indexed) or 50000 (COLLSCAN)Converting ObjectId in API Responses
When findOne or find().toArray() return documents with ObjectId fields, those ObjectIds need special handling before returning them in a JSON API response. JSON.stringify serializes an ObjectId as an object {} (losing the value) in older driver versions, or as its string representation in newer ones.
The safest pattern is to explicitly call .toString() on any ObjectId fields in a mapping function before sending to the client. Clients then send the ID back as a string, and you convert it with new ObjectId(idString) in the server before querying.
function toPublicDoc(doc) {
if (!doc) return null;
return {
...doc,
_id: doc._id.toString(), // ObjectId -> string for JSON
authorId: doc.authorId ? doc.authorId.toString() : null
};
}
// Usage:
const post = await db.collection('posts').findOne({ slug: 'intro' });
res.json(toPublicDoc(post));
// Client receives: { _id: '64a2f3b1c9e7...', title: '...' }Quick Check
Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.
Lesson Recap
In this lesson you learned: findOne returns a single document directly while find returns a cursor that lazily streams results in batches to avoid memory issues with large result sets, cursors support a fluent chain API—.sort(), .limit(), .skip(), .maxTimeMS()—which MongoDB sends as a single optimized query, and explain('executionStats') reveals whether a query uses an index (IXSCAN) or a full collection scan (COLLSCAN). Next up we dive into querying nested fields and arrays with dot notation.
Frequently asked questions
Is the “findOne vs find: Cursors Explained” lesson free?
Yes — the full text of “findOne vs find: Cursors Explained” is free to read here on the web, and the MongoDB Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the MongoDB Academy course, upgrade to CoddyKit PRO.
What will I learn in “findOne vs find: Cursors Explained”?
Learners will retrieve documents using findOne and iterate a find cursor, understanding how MongoDB streams large result sets. You practise MongoDB Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start MongoDB Academy?
No prior experience is required. MongoDB Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “findOne vs find: Cursors Explained” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this MongoDB Academy lesson?
Yes. Every MongoDB Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- insertOne and insertMany
- findOne vs find: Cursors Explained
- Querying Nested Fields and Arrays
- Reading Documents With the Node.js Driver