Mongoose Pagination Patterns Compared: Skip Limit vs Cursor vs Range Queries
paginationmongoosemongodbperformancecomparison

Mongoose Pagination Patterns Compared: Skip Limit vs Cursor vs Range Queries

MMongoose Cloud Editorial
2026-06-11
9 min read

A practical comparison of skip/limit, cursor, and range-query pagination in Mongoose, with tradeoffs, use cases, and when to switch.

Pagination looks simple until a collection grows, users expect stable results, and performance starts to matter. In Mongoose, the three patterns most teams compare are skip/limit, cursor pagination, and range-query pagination. Each can be correct in the right context, but they solve different problems and fail in different ways. This guide compares them as implementation choices rather than abstract database theory, so you can choose a pattern that fits your query shape, sorting rules, and product requirements now—and revisit the decision later as volume and usage change.

Overview

If you are deciding how to paginate MongoDB results in a Mongoose application, the short version is this: skip/limit is easiest to build, cursor pagination is usually the best default for large or active datasets, and range queries are often the most direct option when your data already has a natural ordered field.

That sounds straightforward, but the tradeoffs are more specific than “easy versus fast.” You also need to think about whether users need page numbers, whether inserts happen while a user is browsing, whether your sort field is unique, and whether your API needs forward-only navigation or arbitrary jumps.

Here is the high-level comparison:

  • Skip/limit pagination: good for admin views, small collections, prototypes, and interfaces where page numbers matter more than perfect consistency.
  • Cursor pagination: good for APIs, feeds, timelines, event logs, and most production endpoints where stable navigation and performance matter.
  • Range-query pagination: good when you already page by a field like _id, createdAt, score, or another indexed sortable value and want precise control over the query.

In practice, cursor and range-query approaches overlap. Many developers treat them as the same idea because a cursor is often just an encoded version of the last seen sort values. For implementation decisions, though, it is still useful to separate them:

  • Cursor pagination focuses on the API contract: the client gets a token or key and asks for the next slice.
  • Range-query pagination focuses on the database filter: the next page is fetched with conditions like $gt or $lt on indexed fields.

That distinction matters when you design links, GraphQL connections, REST responses, or SDK behavior.

How to compare options

Before choosing a pagination pattern, compare them against the behavior your application actually needs. The right question is not “which one is best?” but “which failure mode can this endpoint tolerate?”

1. Dataset size and growth rate

Skip/limit can feel fine early on, then degrade as offsets grow. If your collection is expected to become large or highly active, make that part of the decision up front. A pattern that is easy this week can become your next performance cleanup.

If you are benchmarking Mongoose query behavior more broadly, it helps to pair this decision with query-level testing and indexing review. Related reading: Mongoose Query Performance Benchmarks: Common Patterns Compared and Mongoose Indexing Checklist for Faster Queries.

2. Sort stability

Pagination is only predictable when sort order is predictable. If you sort by a field with duplicates, such as createdAt down to the second or a non-unique score, you need a tie-breaker. A common pattern is sorting by createdAt and then _id. Without a stable compound sort, users may see duplicates or missing items between page loads.

3. User experience requirements

Ask what the interface needs:

  • Do users need page 7 specifically?
  • Do they mostly click next and previous?
  • Do they expect the list to remain stable while new records are inserted?
  • Do they need a total count shown beside the results?

Page-number navigation strongly favors skip/limit. Feed-like browsing strongly favors cursor or range-based approaches.

4. API simplicity versus implementation discipline

Skip/limit is simple to explain and debug. Cursor pagination requires a stricter contract: encode sort state, validate cursors, and handle sort changes carefully. Range queries are compact and efficient, but they require you to think clearly about the underlying index and boundary conditions.

5. Index alignment

The best pagination pattern still performs poorly if the index does not match the sort and filter. If your endpoint filters by tenant, status, or visibility before sorting, your compound index should reflect that access path. For multi-tenant systems, pagination design and data partitioning often go together. See How to Structure Mongoose Models for Multi-Tenant SaaS Apps.

6. Consistency under writes

On active collections, new inserts or updates can shift result positions between requests. Skip/limit is most vulnerable because the offset assumes the earlier rows are unchanged. Cursor and range-based pagination are usually more resilient because they continue from the last seen item rather than from a moving row number.

Feature-by-feature breakdown

This section compares the three patterns where developers feel the tradeoffs most clearly: performance, correctness, API design, indexing, and operational complexity.

Skip/limit pagination

Typical shape: find(filter).sort(sort).skip(offset).limit(pageSize)

Why teams choose it: It maps naturally to page numbers and is easy to explain in both UI and API contracts. Most developers can implement it quickly, and it works well enough for low-volume internal tools.

Strengths

  • Simple request model: page and page size.
  • Easy to support numbered pagination.
  • Straightforward for reporting views and back-office tables.
  • Convenient when users need to jump to arbitrary positions.

Weaknesses

  • Large offsets become increasingly inefficient.
  • Results can shift under concurrent writes, causing duplicates or gaps.
  • Deep pagination is often the worst case.
  • Total counts are often requested alongside it, which can add more work to the endpoint.

Best use cases

  • Admin dashboards with modest data volume.
  • Internal tables where approximate browsing is acceptable.
  • Early-stage products where simplicity matters more than long-term scale.

Caution

If you keep skip/limit, put guardrails around it. Cap maximum page size, avoid very deep offsets, and ensure your sort is deterministic. If you also hydrate large Mongoose documents, consider whether lean() would reduce overhead for list endpoints. See Mongoose Lean Queries vs Documents: Performance and Feature Tradeoffs.

Cursor pagination

Typical shape: client sends a cursor token representing the last seen record; server decodes it and queries the next page using the current sort order.

Why teams choose it: It gives stable, efficient next-page navigation without exposing raw offsets. It is a strong fit for APIs and data-heavy interfaces.

Strengths

  • Scales better than offset-based paging for large result sets.
  • More resilient when records are inserted or removed between requests.
  • Works naturally for “load more,” feeds, and infinite scroll.
  • Can encode compound sort state, which helps preserve deterministic ordering.

Weaknesses

  • More complex request and response contract.
  • Harder to support arbitrary page jumps.
  • Requires careful handling when sort options change.
  • Cursor tokens need validation and clear versioning if the format evolves.

Best use cases

  • Public APIs.
  • Activity feeds, audit logs, message lists, event streams.
  • Collections where writes happen frequently during browsing.

Implementation note

A robust cursor usually includes every field needed to continue the sort, not just one value. For example, if results are sorted by createdAt desc, _id desc, the cursor should contain both createdAt and _id. That avoids ambiguity when multiple records share the same timestamp.

Range-query pagination

Typical shape: find({ ...filter, sortField: { $lt: lastValue } }).sort({ sortField: -1 }).limit(pageSize), often with a tie-breaker condition.

Why teams choose it: It is close to the database model, explicit, and efficient when the sorted field is indexed and meaningful.

Strengths

  • Fast and predictable for ordered traversal.
  • Simple mental model when paging by a natural sequence such as _id or createdAt.
  • Works very well for append-heavy datasets.
  • Easy to combine with compound indexes when designed carefully.

Weaknesses

  • Does not inherently provide a user-friendly API contract.
  • Gets more complex with non-unique sort fields.
  • Bi-directional pagination can require extra logic.
  • Not ideal if users need exact page numbers.

Best use cases

  • Timeline-like results.
  • Job histories, deployment logs, audit tables.
  • Any endpoint already anchored to a sortable and indexed business field.

Important nuance

Range queries are only as correct as the field semantics. Paging by _id is often convenient, but only if your application accepts that ordering as a proxy for recency. If your real business sort is publishedAt, priority score, or tenant-specific visibility, page by that logic instead of assuming _id always represents what the user expects.

Comparison summary

  • Ease of implementation: skip/limit wins.
  • Performance at scale: cursor and range queries usually win.
  • Stable results during writes: cursor and range queries are stronger.
  • Numbered pages: skip/limit wins.
  • Feed and API design: cursor often wins.
  • Direct use of indexed sort fields: range queries are especially strong.

No pattern is universally superior. The better choice depends on whether your endpoint behaves more like a report, a feed, or a traversal of an ordered dataset.

Best fit by scenario

If you need a practical recommendation, start here. These scenarios cover the most common Mongoose pagination decisions.

Scenario 1: Internal admin table with filters and page numbers

Recommended pattern: skip/limit, unless the dataset is already large enough to cause visible delay.

Why: admins often expect page numbers, sortable columns, and direct jumps. If the table is not deeply paged and the collection is modest, the simplicity is worth it.

Scenario 2: Public API endpoint for recent items

Recommended pattern: cursor pagination.

Why: API consumers usually care more about stable next-page behavior than numbered navigation. Cursor-based contracts also age better when traffic grows.

Scenario 3: Activity feed or audit log

Recommended pattern: range query or cursor based on createdAt plus _id.

Why: these datasets are naturally ordered and frequently appended to. Offset-based paging is more likely to produce confusing movement between requests.

Scenario 4: Multi-tenant SaaS list endpoint

Recommended pattern: cursor or range query with a compound index that starts with tenant constraints.

Why: tenant filtering changes the access path. A good pagination pattern can still underperform if the index ignores the tenant boundary.

Scenario 5: User-facing search results where exact page numbers matter

Recommended pattern: skip/limit, potentially with strict depth limits.

Why: search interfaces often rely on explicit page counts and page jumps. If this endpoint must support page 12, offset-based paging may still be the least awkward fit.

Scenario 6: High-volume event ingestion with recent-first browsing

Recommended pattern: range query or cursor.

Why: this is the kind of workload where you want ordered traversal, not expensive deep skips.

A practical default

If you are building a new production API and do not have a strong reason for numbered pages, start with cursor pagination backed by a deterministic compound sort. It is usually the most durable default.

If you already have a natural ordered field and want a simple, explicit query plan, range-query pagination may be even better. Use skip/limit when its UX advantages are real and the operational cost is acceptable.

When to revisit

Pagination is not a one-time choice. Revisit it when your usage patterns or data model change. The signs are usually visible before they become outages.

Review your current approach when any of the following happens:

  • Collection growth changes the shape of the query. What worked at thousands of documents may feel different at millions.
  • Users start paging deeply. Admin features often expand quietly until deep offsets become common.
  • Write activity increases. More inserts during browsing can expose duplicate or missing rows in offset-based lists.
  • Sort rules change. Adding secondary sorts or business-priority ordering can invalidate simplistic cursors.
  • You add multi-tenant filtering, visibility rules, or status filters. These often require new compound indexes and sometimes a different pagination model.
  • The frontend changes from numbered pages to infinite scroll. That is often the moment to switch from skip/limit to cursor-based contracts.

Use this action checklist when reassessing an endpoint:

  1. Document the exact filter and sort combination used in production.
  2. Confirm whether the sort is deterministic, including tie-breakers.
  3. Check whether the index order matches the filter-and-sort path.
  4. Measure real-world latency for first page and deeper pages.
  5. Decide whether users truly need page numbers or only next/previous navigation.
  6. Test behavior while writes are happening in parallel.
  7. If changing patterns, plan the migration path for clients and links.

If a pagination change affects schemas, timestamps, or audit behavior, it is worth reviewing related implementation details in Mongoose Timestamps, Defaults, and Auditing Fields: Best Practices. If the change introduces new cursor parsing or query validation logic, also tighten input validation and error handling. Helpful references: Mongoose Validation Patterns That Prevent Bad Data in Production and Mongoose Error Handling Guide for CastError, ValidationError, and Duplicate Keys.

The practical takeaway is simple: choose the pagination pattern that matches your current product behavior, but design it so you can replace it before it becomes expensive. For many teams, that means using skip/limit deliberately, not by default; using cursor pagination for API-first endpoints; and using range queries where the data already has a strong natural ordering. A good pagination strategy is less about picking a winner and more about aligning query mechanics with how people actually browse your data.

Related Topics

#pagination#mongoose#mongodb#performance#comparison
M

Mongoose Cloud Editorial

Senior SEO Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

2026-06-13T11:56:14.656Z