Working with Cursors
1. Understanding Cursor Behavior
| Aspect | Detail |
|---|---|
| Lazy | Server batches results, client pulls as needed |
| Default batch | 101 docs first batch, 16MB subsequent |
| Idle timeout | 10 minutes (server) |
| Resource | Holds locks/snapshots on server |
2. Iterating Cursor Results
| Pattern | Description |
|---|---|
| for await (const d of cursor) | Driver async iteration |
| cursor.forEach(fn) | Imperative |
| while (cursor.hasNext()) | mongosh |
| cursor.tryNext() | Non-blocking peek |
3. Converting Cursor to Array
| Method | Warning |
|---|---|
| toArray() | Loads all results in memory |
| Use case | Small bounded results only |
| Anti-pattern | toArray() on unbounded find() |
4. Counting Cursor Results
| Method | Notes |
|---|---|
| countDocuments(filter) | Accurate; uses aggregation |
| estimatedDocumentCount() | Fast, from metadata; no filter |
| cursor.count() DEPRECATED | Replaced by countDocuments |
5. Setting Cursor Limit
| Method | Description |
|---|---|
| .limit(n) | Cap returned docs |
| Negative limit | Single batch, then close cursor |
6. Skipping Cursor Results
| Aspect | Detail |
|---|---|
| .skip(n) | Skip n docs server-side |
| Performance | O(n) cost; avoid large skips for pagination |
| Better pagination | Range query on indexed sort key |
Example: Range-based pagination
// Page N uses last _id of page N-1
const page = await db.posts.find({ _id: { $gt: lastId } }).sort({ _id: 1 }).limit(20).toArray();
const lastId = page.at(-1)?._id;
7. Sorting Cursor Results
| Form | Effect |
|---|---|
| .sort({field: 1}) | Ascending |
| .sort({field: -1}) | Descending |
| .sort({a:1, b:-1}) | Composite |
| In-memory limit | 32MB unless backed by index |
| $meta sort | By text score: .sort({score: {$meta: "textScore"}}) |
8. Setting Batch Size
| Method | Effect |
|---|---|
| .batchSize(n) | Docs per network round-trip |
| Trade-off | Small = lower memory, more RTT; large = opposite |
| Cap | Server limits batches to 16MB regardless |
9. Using Tailable Cursors
| Option | Purpose |
|---|---|
| tailable | Stays open at end of capped collection |
| awaitData | Block briefly for new docs |
| Use case | Log tailing, change-stream-like |
10. Using noCursorTimeout Option
| Aspect | Detail |
|---|---|
| Purpose | Prevent server-side 10-minute idle close |
| Risk | Cursor leaks if client forgets to close |
| Use case | Long-running ETL / batch jobs |
11. Closing Cursors
| Method | Effect |
|---|---|
| cursor.close() | Frees server resources immediately |
| killCursors command | Server-side close by id |
| Auto-close | When exhausted or idle timeout |
12. Understanding Cursor Expiration
| Condition | Result |
|---|---|
| 10-min idle | Server closes; CursorNotFound error |
| Connection drop | Cursor invalidated |
| noCursorTimeout | Disables idle expiration |
| Recovery | Re-issue query; cannot resume |