findPage method detailed documentation
Totals cache keys distinguish BSON/RegExp queries and the effective count options, including inherited let, collation and hint. Parameterized $expr filters receive let in both reads and counts, preserving BSON document field order. Defined top-level options override nested options; totals-specific hint/collation/maxTimeMS overrides retain their priority. Session reads bypass totals caching. Bookmark keys preserve compound-sort order and share effective query options with maintenance APIs. Totals and bookmark v4 keys start cold; old entries leave through TTL or existing collection invalidation.
The shared v4 encoder separates undefined document members, Code.scope, negative zero, native binary values and captured function sources. Set native BSON flags in options; operation/collection/default inheritance is shared by page reads, totals and bookmark maintenance. Functions are captured per occurrence once within a logical page call and reused across hops and totals; independent calls capture again. Page pipeline keys retain executed literals, including string/ObjectId differences with automatic conversion enabled. Upgrading changes page/totals/bookmark hashes and requires cold prewarming; see cache migration.
Nonstandard BSON result modes bypass page-result caching. Static fieldsAsRaw maps are captured with prototype data properties. Dynamic Proxy/accessor maps retain native lazy lookup and bypass page-result and bookmark reuse; unused getters are not evaluated while building keys. prewarmBookmarks and dimensioned listBookmarks/clearBookmarks reject dynamic maps with INVALID_ARGUMENT; broad list/clear without dimensions remains available. Totals exclude this decoding-only map from their fingerprint and preserve completion tokens and async polling.
Overview
findPage is read-only: a top-level $out or $merge in pipeline throws INVALID_OPERATION before cache or database access. Pipeline structure is captured when called. Use aggregate for write pipelines, subject to write-path policy.
With a custom pipeline, projection/project is applied after the user stages, and cursor sort fields remain in returned items. Inclusion, exclusion and array projections are supported. Find-only $slice, $elemMatch and positional projections with a pipeline throw INVALID_OPERATION; use aggregation expressions with aggregate instead. Without a pipeline, native find projections keep their existing path.
findPage is an advanced paging query method provided by monSQLize. It supports multiple paging modes, including cursor paging, page jump, streaming query, and total statistics.
Method signature
Parameter description
options object properties
jump configuration item
Bookmark mechanism for optimizing page jump performance.
Bookmark mechanism description:
- Bookmarks are saved under
<cache-namespace>:bm:; the namespace includes source/authentication fingerprints, pool, database, collection and optional instance identity. - Bookmark keys hash actual query values, normalized sort, limit, pipeline, cursor type hints and normalizer version; queries with different values do not share anchors.
- Default TTL is 6 hours (configurable via
defaults.bookmarks.ttlMs) - Save bookmarks for up to 10000 pages (configurable via
defaults.bookmarks.maxPages)
offsetJump configuration item
Use the traditional offset method to jump pages (suitable for small data volumes).
Performance Suggestion: Although offset page jumping is simple, its performance is poor on large data sets, and it is only suitable for scenarios where the amount of data is less than 100,000.
totals configuration items
Used to obtain the total number and total page number information.
totals mode description
- none: Do not count the total number (default), best performance
- sync: Synchronize statistics, return the total number immediately, which may affect response time (suitable for scenarios with small data volume or index optimization)
- async: Asynchronous statistics, returning token for the first time, caching the results after background calculation (suitable for large data volumes)
- approx: Fast approximate-style statistics. Empty queries use
estimatedDocumentCount; filtered queries usecountDocumentswith the providedhint/collationso the query condition is respected.
Note:
- Statistical results are cached with the
findPageTotals:key prefix and thettlMsvalue. - When statistics fails,
total: nullwill be cached with theerrorfield - async mode uses inflight deduplication while the same totals calculation is still running.
Return value
Normal mode returns object
Streaming mode return
When stream: true is used, a MongoDB Cursor Stream object is returned, and the streaming API can be used:
Streaming Mode Limitations:
- The page jump function is not supported (the page parameter can only be 1 or omitted)
- Does not support totals statistics
- Only supports cursor paging (after/before) or homepage query
- Returns the original stream object, excluding pageInfo
Usage mode
1. Cursor paging (recommended)
Cursor paging is the most efficient paging method and is suitable for large data sets and real-time data.
Advantages:
- O(1) performance, independent of data volume
- Support real-time data changes
- Small memory footprint
Notice:
- The cursor contains the value of the sorting field, and the sorting rules must be consistent
- When
cursorSecretis not configured, the cursor is purely Base64url encoded, and the client can decode the content; after configuration, an HMAC-SHA256 signature is appended, and the tampered cursor will be rejected by the server. - Cursor comparison restores JSON-round-tripped values before building the MongoDB filter: by default, 24-character hexadecimal strings are treated as ObjectId values, and ISO timestamp-like strings are treated as Date values. Use
cursorTypesorcursorValueNormalizerwhen a string sort field intentionally contains ObjectId-like or ISO-like values. - Cursor anchors read sort values by dot path, so nested sort fields such as
{ 'metrics.rank': 1 }are supported. - Do not splice or modify the cursor on the client side
- If you need long-term cross-session persistence of the cursor, please also cooperate with the server's own expiration control.
Cursor signing (recommended in production):
After enabling, the format of endCursor / startCursor becomes <payload>.<signature>, and the server automatically verifies the signature every time findPage is called. Throws INVALID_ARGUMENT error when signatures do not match.
Upgrade Note: If you generated cursor tokens during the running of v1 and persisted them (such as saving them to the database), after turning on
cursorSecretin v2, these old tokens will become invalid due to the lack of signature fields. See the "Cursor Token Upgrade Strategy" section below for the migration plan.
If the sort field is a string that can look like a Date or ObjectId, pin its cursor type:
2. Page jump mode
Suitable for scenarios where random access to any page number is required.
Bookmark Jump Principle:
- Save a bookmark (cursor) every
steppage - When jumping, first locate the recent bookmark page
- Jump from bookmark page to target page page by page
- The number of jumps does not exceed the
maxHopslimit
Bookmarks created by prewarmBookmarks() are read by findPage() during page navigation. When a matching bookmark exists, maxHops is evaluated from that bookmark page to the requested page, not from page 1.
Bookmark maintenance APIs
Bookmark maintenance belongs to the findPage() pagination workflow. Use these APIs when page jumps need predictable latency, or when operations staff need to inspect or reset stored page bookmarks.
keyDims should describe the same query, sort, and limit shape used by findPage(). The runtime normalizes stable sort fields so the maintenance APIs address the same bookmark group as page navigation.
For full return shapes and operational examples, see the bookmark maintenance API reference.
Applicable scenarios:
- Need to display page number navigation
- Users may jump to any page
- Data is relatively stable
3. Streaming query
Suitable for processing large amounts of data and reducing memory usage.
Usage Suggestions:
- Set a reasonable
batchSize(recommended 100-1000) - Use
limitto limit the amount of returned data (to prevent infinite streaming) - Handle error events properly
- Consider backpressure control
4. Get total statistics
Best Practice:
- Small data size (< 100,000): use
syncmode - Large data volume: use
asyncmode to avoid blocking - Properly configured
maxTimeMSto prevent slow queries - Use
hintto specify index optimizationcountDocuments
5. View execution plan (explain)
The explain parameter can help you analyze query performance and see how MongoDB performs paginated queries.
explain mode description:
Usage Tips:
- Index verification: Use
explain: trueto confirm whether the query uses the expected index - Performance Analysis: Use
explain: 'executionStats'to view the actual number of scanned documents - Optimization Index: Pay attention to the ratio of
totalDocsExaminedtonReturned, which should ideally be close to 1 - Do not cache results: Results will not be cached in
explainmode and will not affect normal query caching.
Note:
explainmode will directly return the execution plan object and will not return the paging results.- Cannot be used with
streammode explainapplies to all paging modes (cursor, page jump, offset)
Error handling
Common error codes
Error handling example
Performance optimization suggestions
1. Index optimization
Make sure you have appropriate indexes on the query and sort fields:
Index design principles:
- The query field comes first and the sorting field comes last
- Including
_idas the last field ensures uniqueness - Use
explain()to verify index usage
2. Choose the paging mode reasonably
- Cursor paging: suitable for sequential browsing, real-time data, and large data sets
- Jump Page Mode: Suitable for scenarios where random access to page numbers is required
- offset page jump: only suitable for small data volume (< 10000 items)
- Streaming query: suitable for batch processing, ETL, export and other scenarios
3. Caching strategy
Caching Best Practices:
- Enable caching for popular queries
- Set a reasonable TTL based on the data update frequency
- Writes do not invalidate
findPageread caches by default; usecache.invalidate,autoInvalidate: true, or callcollection.invalidate('findPage')manually when needed.
4. Correctly handle total statistics
❌ Wrong practice: Using synchronized statistics for large amounts of data
✅ Correct approach: Choose the appropriate statistical mode based on the amount of data
5. Correct use of streaming queries
6. Page jump performance optimization
❌ Wrong practice: Improper configuration when jumping to a remote page
✅ Correct approach: Optimize jump page configuration based on usage patterns
Jump page configuration suggestions:
- < 100 pages: step = 5-10, maxHops = 20
- 100-1000 pages: step = 10-20, maxHops = 30-50
- >1000 pages: Consider using cursor paging or limiting the jumpable range
- Small data size: Use offsetJump first
Frequently Asked Questions (FAQ)
Q: What is the difference between cursor paging and traditional paging?
A: Cursor paging uses the sort value as the anchor point, and the performance is O(1); traditional paging uses skip/offset, and the performance decreases as the page number increases. Cursor paging is more suitable for large data sets and real-time data.
Q: Why do I need to set maxHops for page jumps?
A: Prevent malicious or incorrect requests from causing excessive jumps. Each jump is a database query, and the maxHops limit protects system performance.
Q: When should streaming mode be used?
A: Used when processing a large amount of data that does not need to be loaded into memory at once, such as data export, batch processing, ETL and other scenarios.
Q: Will the totals statistic affect performance?
A: sync mode will affect response time, it is recommended to use async mode. The first query triggers background statistics, and subsequent queries return cached results.
Q: How much memory will bookmarks take up?
A: Approximately 200-500 bytes per bookmark. The default cache is up to 10,000 pages, and the total memory usage is about 2-5 MB, which can be adjusted through configuration.
Q: How to deal with cursor failure caused by data changes?
A: Capture CURSOR_INVALID error and restart the query from the home page. For data that changes frequently, it is recommended to use stable fields such as timestamp to sort.
Q: Can the page, after, and before parameters be used at the same time?
A: No. These three parameters are mutually exclusive:
pageis used for page skip mode and cannot be used simultaneously withafterorbeforeafterandbeforeare used for cursor paging, and they cannot be used at the same time.- Using both will throw
VALIDATION_ERRORerror
Q: How to implement the "previous page" function when the cursor is paging?
A: Use the before parameter in combination with startCursor:
Q: Why does streaming mode not support totals statistics?
A: The design goal of streaming mode is to efficiently process large amounts of data. What is returned is the original MongoDB Cursor Stream, which does not contain paging meta information. If total statistics are required, you should:
- First use normal mode to get the homepage and total number
- Then use streaming mode to process subsequent data
Q: How to determine whether the last page has been reached?
A: Use pageInfo.hasNext field:
For cursor paging, it can also be judged by hasNext:
Q: What should I do if the JUMP_TOO_FAR error occurs when jumping to a page?
A: This error indicates that the jump distance exceeds the maxHops limit. There are several solutions:
Option 1: Increase maxHops value
Option 2: Reduce the step value (dense bookmarks)
Option 3: Downgrade to offset page jump
Q: How to optimize the total statistics performance of large amounts of data?
A: The following strategies are recommended:
1. Use async mode + long cache
2. Specify index for statistical query
3. Only count when needed, and judge by hasNext at other times
Q: Does explain mode return actual data?
A: No. When using the explain parameter, findPage will directly return the MongoDB execution plan object without returning the actual paging data and pageInfo.
Usage Suggestions: explain is mainly used to analyze query performance during the development and debugging stages and should not be used in normal requests in the production environment.
Q: How does caching work? Will the data automatically become invalid after being updated?
A: Caching mechanism description:
Cached content:
- Query result cache: the key prefix is
findPage:, and it stores non-stream/non-explain page results whencacheis greater than0 - Bookmark cache: the key prefix is
<cache-namespace>:bm:, and it stores jump bookmarks - Totals cache: the key prefix is
findPageTotals:, and it stores totals results controlled bytotals.ttlMs
Cache Invalidation:
- TTL controls automatic expiration.
- Writes through the monSQLize collection accessor clear read caches only when
cache.invalidate,autoInvalidate: true, or globalcache.autoInvalidateis configured. - If data is changed through the native MongoDB driver, another process, or an external job, manually invalidate the related cache.
Cache key distinction:
- Matching query, normalized sort, limit, projection, pipeline, cursor and cursor strategy can reuse results within the same cache scope.
- Different query conditions will generate different cache keys
- Custom normalizers need a stable version for page-result caching and automatic bookmark reuse; raw key formats are internal. See Cache Invalidation.
Q: What is the scope of pipeline parameters?
A: pipeline only takes effect on the limit data** returned on the current page and does not affect the paging logic and query conditions.
Note:
- The pipeline is executed after the paging logic**
- It will not affect the totals statistics (the statistics are raw data)
- Will not affect cursor calculation (cursor is based on original sorting field)
- Suitable for post-processing such as data association and field conversion
Q: How to achieve infinite scroll loading?
A: Use cursor paging with front-end status management:
Front-end example (React):
Backend Example (Node.js):
Q: What is the difference between findPage and the ordinary find method? How to choose?
A: The two applicable scenarios are different:
Scenarios for using find:
- Simple query, no paging required
- The amount of data is certain and small (< 1000 items)
- All data needs to be returned at once
- No need for advanced features such as cursors and bookmarks
Scenarios for using findPage:
- Need to be displayed in pages
- The amount of data is large or uncertain
- Need to support "next page", "previous page", page jump and other functions
- Need total statistics
- Requires streaming of big data
- Need to cache paginated results
Example comparison:
Performance comparison:
Cursor token security and upgrade strategy
The role of cursorSecret
cursorSecret is an optional instance-level configuration item. When not set, the cursor token is pure Base64url encoded and anyone can decode or construct its contents (including the sort field value). Once set, each token will have an HMAC-SHA256 signature attached:
The server automatically verifies the signature every time findPage is called. If the token has been tampered with (for example, the client manually modified the sorting field value to skip data), the signature does not match and the server throws an INVALID_ARGUMENT error. The signature comparison is performed with a timing-safe comparison in the current runtime.
It is recommended to always configure cursorSecret in production environment:
requireCursorSecret is optional and defaults to false for compatibility. When set to true, findPage() throws INVALID_CONFIG until cursorSecret is configured, preventing unsigned cursor tokens from being emitted or accepted.
Cursor value type hints
Cursor payloads are JSON encoded, so Date and ObjectId values are restored before MongoDB comparison filters are built. The default restoration preserves existing behavior: ISO timestamp-like strings become Date values and 24-character hex strings become ObjectId values.
Use cursorTypes when a sort field should be restored differently:
For custom conversion logic, pass cursorValueNormalizer(field, value) at the instance level or on a single findPage() call.
When using result caches or prewarmed bookmarks, also provide cursorValueNormalizerVersion, a stable strategy identity and version such as token-string:v1. Change it whenever conversion logic, captured configuration or restored types change. A given identifier must always describe the same conversion behavior.
The instance configuration accepts the same field. A per-query function override needs an explicit per-query version; it does not inherit the instance function's version. An unversioned custom function can still query, but bypasses page-result caching and automatic bookmark reads; bookmark prewarming rejects it. Deep jumps may therefore need more database queries and reach jump.maxHops. Both cursorTypes and the strategy version participate in page and bookmark keys.
A custom normalizer replaces default restoration and cursorTypes for every sort field, including the _id tie-breaker. Handle Date/ObjectId fields explicitly; returning an unchanged ObjectId string can produce incorrect cursor comparisons. An exclusion projection such as { _id: 0 } retains business fields, and all cursor sort fields (including _id) remain in the returned items.
Cursor comparisons support null/missing boundaries and common scalar BSON sort types. Pagination uses $expr comparisons; verify index efficiency with explain for your query, sort and data distribution. Array sorting by minimum/maximum elements is outside this guarantee.
Cursor token compatibility issues when upgrading from v1
Source of the problem: If you persistently store endCursor / startCursor (write to database, put into Redis, encode in URL) during v1 running, after v2 turns on cursorSecret, these old token formats will be <payload> (no signature), and v2 will fail to parse because the signature separator cannot be found during v2 verification.
Will it affect you:
Migration plan
Option A (recommended): Two-stage deployment
- Do not set
cursorSecretwhen v2 goes online, maintaining the same unsigned format as v1 - Wait for the TTL of the persistent cursor to expire naturally (usually hours to days)
- After expiration, release the second deployment and add
cursorSecret - At this time, all cursors in transit have expired, and no old format tokens will flow in.
Option B: Actively clear persistent cursors
Before v2 deployment, clear all persisted cursor data (set database fields to null, delete Redis key, etc.), and then directly bring cursorSecret online. The user's pagination status is reset to the first page.
Scenario C: Custom error downgrade
Catch the INVALID_ARGUMENT error at the API layer and reset the cursor to null (i.e. back to the first page):