Results, Sorting & Pagination
Cursor-based Pagination
Keyset pagination with a stable sort — the pattern production APIs actually use.
A cursor is 'give me the next N after this document', not 'skip 20000'. The client sends the last price and _id it saw. The filter is a range on the sort keys. The server never walks the rows you already returned.
db.products
.find({ inStock: true })
.sort({ price: 1, _id: 1 })
.limit(10)// last = { price: 4500, _id: ObjectId("64a1b2c3d4e5f6a7b8c90021") }
db.products.find({
inStock: true,
$or: [
{ price: { $gt: 4500 } },
{ price: 4500, _id: { $gt: ObjectId("64a1b2c3d4e5f6a7b8c90021") } }
]
})
.sort({ price: 1, _id: 1 })
.limit(10)That $or is 'later in the (price, _id) order'. Descending feeds (createdAt: -1) use $lt instead. The sort must match the range, and the last key must be unique (_id) or two products at ₹4500 collide and you skip one. Encode price + _id as an opaque next token in the API; do not let clients invent skip values.
- You cannot jump to 'page 50' without walking 50 pages (or storing tokens). Offset can. That is the UX trade.
- Inserts behind the cursor do not duplicate the next page. Inserts ahead of an offset page do.
- An index on
{ inStock: 1, price: 1, _id: 1 }(later) makes this an index range, not a scan.
Interview question
How does cursor (keyset) pagination work in MongoDB?
Sort by a stable key, return N documents, next request filters to rows after the last key — $gt / $lt on that sort, with _id as a unique tiebreaker. The server does not skip the previous pages. I cannot jump to page 50 without extra work; that is the trade for speed and stability.