Tình huống gặp lỗi
Bạn đang duyệt qua một query MongoDB lớn — xử lý hàng nghìn document, chạy migration, hoặc xuất dữ liệu — và giữa chừng bạn gặp lỗi này:
MongoServerError: cursor id not found
Vài trăm document đầu tiên xử lý bình thường. Rồi cursor chết đột ngột. Script của bạn bị crash giữa chừng và phải chạy lại từ đầu.
Hầu hết trường hợp, đây là vấn đề cursor timeout. Cursor phía server của MongoDB có thời gian idle timeout mặc định là 10 phút. Nếu quá trình xử lý bị chậm giữa các lần fetch batch — tính toán nặng, gọi API bên ngoài, dataset lớn — server sẽ hủy cursor. Khi driver của bạn yêu cầu batch tiếp theo, cursor ID đó không còn tồn tại nữa.
Nguyên nhân gây ra lỗi
MongoDB cursor hoạt động theo batch. Một lệnh find() tạo ra một cursor trên server và trả về batch đầu tiên — mặc định 101 document hoặc khoảng 1MB. Driver của bạn lưu cursor ID và yêu cầu thêm batch khi bạn tiếp tục duyệt.
Điểm mấu chốt ở đây: cursor tồn tại trên server, không phải trong ứng dụng của bạn. Nếu cursor ở trạng thái idle quá 10 phút mà không có yêu cầu batch mới, MongoDB sẽ tự động dọn dẹp nó. Lần tiếp theo driver gửi cursor ID đó, server không còn nhận ra nữa.
Các nguyên nhân thường gặp:
- Xử lý từng document quá chậm bên trong vòng lặp — gọi API, ghi disk, tính toán nặng mất 2–5 giây mỗi document
- Dataset mà một batch 200 document mất hơn 10 phút để xử lý
- Ngắt kết nối mạng làm tạm dừng quá trình duyệt
- Cursor được mở nhưng không được tiêu thụ ngay
- Web app rơi vào trạng thái idle giữa các request khi đang duyệt
Cách fix nhanh: tắt cursor timeout
Cách nhanh nhất: báo cho MongoDB không bao giờ timeout cursor. Đó là flag noCursorTimeout.
Node.js (mongodb driver):
const cursor = db.collection('orders')
.find({ status: 'pending' })
.addCursorFlag('noCursorTimeout', true);
for await (const doc of cursor) {
// xử lý chậm cũng không sao nữa
await processDocument(doc);
}
// Luôn đóng cursor khi xong
await cursor.close();
Node.js (mongoose):
const cursor = Order.find({ status: 'pending' })
.cursor()
.addCursorFlag('noCursorTimeout', true);
for await (const doc of cursor) {
await processDocument(doc);
}
await cursor.close();
Python (pymongo):
cursor = db.orders.find(
{'status': 'pending'},
no_cursor_timeout=True
)
try:
for doc in cursor:
process_document(doc) # xử lý chậm cũng không sao
finally:
cursor.close() # bắt buộc — luôn đóng thủ công
Lưu ý quan trọng: khi tắt timeout, MongoDB sẽ không bao giờ tự dọn dẹp cursor. Bạn bắt buộc phải gọi cursor.close() trong block finally. Bỏ qua bước này và script bị crash giữa chừng? Cursor sẽ bị rò rỉ, chiếm bộ nhớ server, và tồn tại mãi cho đến khi MongoDB khởi động lại hoặc bạn tự tay kill nó.
Giải pháp lâu dài: không phụ thuộc vào cursor tồn tại lâu
Tắt timeout chỉ là vá tạm. Giải pháp thực sự là tái cấu trúc code để cursor không bao giờ mở đủ lâu để hết hạn.
Tùy chọn 1: phân batch bằng skip/limit
Chia công việc thành các chunk nhỏ. Mỗi chunk mở một cursor mới và hoàn thành trong vài giây:
const BATCH_SIZE = 500;
let skip = 0;
while (true) {
const docs = await db.collection('orders')
.find({ status: 'pending' })
.skip(skip)
.limit(BATCH_SIZE)
.toArray();
if (docs.length === 0) break;
for (const doc of docs) {
await processDocument(doc);
}
skip += BATCH_SIZE;
}
Mỗi lệnh toArray() fetch và đóng cursor ngay lập tức. Không có cursor mở, không có rủi ro timeout.
Tùy chọn 2: phân batch cursor với maxTimeMS
Cần streaming nhưng muốn có mạng lưới an toàn? Đặt maxTimeMS để cursor báo lỗi nhanh thay vì treo vô thời hạn:
const cursor = db.collection('orders')
.find({ status: 'pending' })
.maxTimeMS(30000) // báo lỗi nếu cursor idle > 30 giây
.batchSize(200); // fetch 200 doc mỗi lần round trip
for await (const doc of cursor) {
await processDocument(doc);
}
Tùy chọn 3: aggregation + $out cho bulk transform
Đang chạy migration hay job chuyển đổi dữ liệu? Đẩy công việc vào MongoDB thay vì kéo document về ứng dụng:
await db.collection('orders').aggregate([
{ $match: { status: 'pending' } },
{ $addFields: { processedAt: new Date() } },
{ $out: 'orders_processed' } // ghi kết quả vào collection mới
]).toArray();
Aggregation chạy hoàn toàn phía server. Không có cursor nào đi qua mạng, không có rủi ro timeout, không có round trip từng document.
Tùy chọn 4: phân trang theo _id thay vì skip/limit
Với collection lớn — khoảng 1 triệu document trở lên — skip trở nên tốn kém. MongoDB phải quét qua tất cả document phía trước mỗi lần phân trang. Phân trang theo _id tránh hoàn toàn vấn đề này:
const BATCH_SIZE = 500;
let lastId = null;
while (true) {
const query = lastId
? { status: 'pending', _id: { $gt: lastId } }
: { status: 'pending' };
const docs = await db.collection('orders')
.find(query)
.sort({ _id: 1 })
.limit(BATCH_SIZE)
.toArray();
if (docs.length === 0) break;
for (const doc of docs) {
await processDocument(doc);
}
lastId = docs[docs.length - 1]._id;
}
Mỗi batch sử dụng một index scan mới, nhanh chóng. Đây là pattern tiêu chuẩn để quét collection lớn.
Kiểm tra sau khi fix
Đừng chỉ giả định là đã xong. Hãy chạy các kiểm tra sau.
1. Kiểm tra cursor đang mở trên server:
// Trong mongosh
db.serverStatus().metrics.cursor
Theo dõi open.noTimeout. Nếu bạn đang dùng noCursorTimeout, con số đó phải về 0 sau khi script hoàn thành — xác nhận các cursor đã đóng sạch.
2. Giả lập xử lý chậm:
// Thêm độ trễ có chủ ý để stress-test bản fix
for await (const doc of cursor) {
await new Promise(r => setTimeout(r, 100)); // 100ms mỗi doc
}
// 1.000 doc × 100ms = 100 giây — vượt mốc 10 phút ở quy mô lớn
// Với bản fix đã áp dụng, quá trình này phải hoàn thành mà không gặp lỗi
3. Quét MongoDB logs để kiểm tra hoạt động cursor:
grep -i "cursor" /var/log/mongodb/mongod.log | tail -20
Tóm tắt
MongoServerError: cursor id not found= cursor phía server đã hết hạn sau 10 phút idle- Fix nhanh:
noCursorTimeout: true— nhưng bắt buộc phải gọicursor.close()trong blockfinally, không có ngoại lệ - Fix tốt hơn: phân batch bằng
skip/limithoặc phân trang theo_id— cursor tồn tại ngắn, không có rủi ro timeout - Bulk transform: chạy aggregation +
$outhoàn toàn phía server để tránh hoàn toàn vấn đề cursor

