Skip to content

Results, Sorting & Pagination

Cursor-based Pagination

Keyset pagination with a stable sort — the pattern production APIs actually use.

IntermediateAbout 7 minutes

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)
First page — no cursor
// 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)
Next page — after the last product you returned

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?

Think about it first.