What it means
A query that returns many documents doesn’t send them all at once. The server sends a first batch
and keeps a cursor open; the driver asks for the next batch (a getMore command) when your code
runs out. If the cursor is gone by then, the server answers with code 43, CursorNotFound:
cursor id 4184835440633628902 not found. The documents you already got are fine; the rest weren’t
read.
The server closes a cursor when:
- it has been idle for 10 minutes (
cursorTimeoutMillis), for cursors not tied to a session; - its session expires: sessions idle for 30 minutes (
localLogicalSessionTimeoutMinutes) are closed with their cursors, and drivers open every cursor in a session, so this applies to most of them, including those opened withnoCursorTimeout; - someone kills it (
killCursors, orkillOpon the operation); - the server restarts or fails over to another member.
Common causes
- Slow work between batches. A script iterates a large cursor and does something slow for each document (an API call, another query), so a batch takes longer to process than the timeout.
- Large batches. Each batch takes so long to process that the gap between
getMorecalls passes the limit. - A load balancer or proxy sends the
getMoreto a differentmongosor server than the one holding the cursor. - A restart or failover in the middle of a long read.
- The cursor was killed by an administrator or a monitoring tool.
How to fix it
Fetch first, work later
Read what you need into memory or a queue, then do the slow work. Or project only the fields you need so each pass over a batch is quick.
Page through by _id instead of keeping one cursor open
Each page is a short new query, so no cursor stays open between them:
let last = null
while (true) {
const page = db.events.find(last ? { _id: { $gt: last } } : {})
.sort({ _id: 1 }).limit(1000).toArray()
if (page.length === 0) break
for (const doc of page) { /* slow work */ }
last = page[page.length - 1]._id
}
Unlike skip, it stays fast on large collections because _id is indexed. If the job stops, it can
resume from the last _id.
Use smaller batches
.batchSize(100) makes the driver ask for more often, so each gap is shorter. It helps when each
document is slow to process.
Keep a long-lived cursor alive
noCursorTimeout() stops the 10-minute idle timeout, but not the 30-minute session timeout. For a
cursor that must stay open longer, start an explicit session, open the cursor in it, and refresh the
session (the refreshSessions command) more often than every 30 minutes. Close the cursor when you’re
done, or it stays open on the server.
Behind load balancers
Make sure the driver connects in load-balanced mode (loadBalanced=true) where the provider
requires it, or that the balancer keeps a client on one server.
Reproduce it
MongoDB 8.0.32, mongosh 2.12.0, 5,000 documents. A cursor opened with a batch size of 2, then killed
before the next getMore:
const r = db.runCommand({ find: "events", batchSize: 2 })
db.runCommand({ killCursors: "events", cursors: [r.cursor.id] })
db.runCommand({ getMore: r.cursor.id, collection: "events" })
MongoServerError[CursorNotFound]: cursor id 4184835440633628902 not found
The error’s code was 43. A cursor iterated with .next() after it was killed returned what was
left of its first batch, then failed with the same message. Open cursors can be listed (and their
ids found) with:
db.getSiblingDB("admin").aggregate([
{ $currentOp: { idleCursors: true } },
{ $match: { type: "idleCursor" } }
])
We tried to reproduce the timeout itself in a temporary container of our own (mongo:8.0, removed
afterwards) by lowering cursorTimeoutMillis to 5 seconds. Cursors from mongosh survived 8 seconds
idle, because mongosh opens every cursor in a session, and those follow the session’s lifetime
instead; the server’s documentation says the same. We didn’t wait out the 30-minute session timeout.
In Inlet
Inlet’s grid reads MongoDB collections a page at a time, with filters and sorting that run on the
server. Queries use mongosh syntax, so a page of the _id loop above
(db.events.find({ _id: { $gt: <last id> } }).sort({ _id: 1 }).limit(1000)) runs in a query tab.