Download

CursorNotFound: cursor id not found

You were reading results in batches and the server no longer had the cursor when you asked for the next batch: it timed out while idle, its session expired, it was killed, or the request went to another server. Fetch faster, or page through the data by _id.

MongoDB error 43· Tested on MongoDB 8.0.32 (mongosh 2.12.0)· Updated 11 October 2026

cursor id 4184835440633628902 not found

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 with noCursorTimeout;
  • someone kills it (killCursors, or killOp on the operation);
  • the server restarts or fails over to another member.

Common causes

  1. 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.
  2. Large batches. Each batch takes so long to process that the gap between getMore calls passes the limit.
  3. A load balancer or proxy sends the getMore to a different mongos or server than the one holding the cursor.
  4. A restart or failover in the middle of a long read.
  5. 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.

Inlet: a database client for the Mac

One native app for PostgreSQL, MySQL, SQL Server, SQLite, MongoDB and Redis. It explains errors where they happen, holds your edits until you save them, and keeps production read-only until you say so.

Version 0.1.0 · macOS 26 Tahoe or later · Apple silicon and Intel