Pagination: skip/limit vs Range Queries
8 min read
Efficient Pagination in MongoDB
Naive skip() + limit() pagination works fine for the first few pages, but MongoDB must count and discard all skipped documents. On page 1000 with 20 items per page, MongoDB scans 19 980 documents before returning 20 — this gets slower with every page.
skip/limit — Simple But Slow at Depth
js
// Page N (1-indexed), pageSize items per page
async function getPage(page, pageSize) {
return db.collection("students")
.find({})
.sort({ _id: 1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
.toArray();
}
// Fine for < 1000 total documents; gets slow beyond thatRange (Cursor) Pagination — Scalable
Instead of skipping, remember the last `_id` of the previous page and use it as a $gt filter. Because _id is always indexed, this query is O(1) regardless of page depth.
js
// First page
let lastId = null;
const pageSize = 20;
async function nextPage(lastId, pageSize) {
const filter = lastId ? { _id: { $gt: lastId } } : {};
const docs = await db.collection("students")
.find(filter)
.sort({ _id: 1 })
.limit(pageSize)
.toArray();
// Save the last _id for the next call
if (docs.length > 0) {
lastId = docs[docs.length - 1]._id;
}
return { docs, lastId };
}Range pagination does not support jumping to an arbitrary page number — users can only go forward (or backward if you track both ends). It is ideal for infinite-scroll UIs and API cursors.
Ready to test yourself?
Practice MongoDB— quiz & coding exercises