Apex Cursor-Based Pagination in Salesforce: A Production-Ready Guide with Next and Previous Page…
Most Salesforce developers learn pagination using OFFSET.
Apex Cursor-Based Pagination in Salesforce: A Production-Ready Guide with Next and Previous Page Support
Most Salesforce developers learn pagination using OFFSET.
Example:
SELECT Id, Name
FROM Account
ORDER BY CreatedDate
LIMIT 50 OFFSET 2000
This query returns records starting after the first 2,000 rows.
To return records 2001–2050, Salesforce must first locate and skip the first 2,000 records.
This creates two significant limitations:
- Performance Degrades as Offset Increases
Each page request requires Salesforce to process all skipped rows before returning the requested records. Larger offsets generally result in more database work.
- OFFSET Has a Hard Limit
Salesforce enforces a maximum OFFSET value of 2,000.Attempting to exceed this limit results in an exception.
Example:
SELECT Id, Name
FROM Account
ORDER BY CreatedDate
LIMIT 50 OFFSET 2001
will result in below exception:
NUMBER_OUTSIDE_VALID_RANGE: Maximum SOQL offset allowed is 2000
For applications that need to navigate beyond the first few thousand records, OFFSET is no longer a viable long-term solution.
This is where cursor-based pagination becomes the preferred architecture.
What is Cursor-Based Pagination?
Instead of requesting:
Give me Page 20
you request:
Give me the next 50 records after this position.
That position is called a cursor.
Common cursor candidates include:
- CreatedDate
- LastModifiedDate
- Auto Number
- External Id
- Indexed Custom Fields
For Salesforce, the most reliable cursor pattern is:
CreatedDate + Id
This combination creates a unique and stable position for every record.
Understanding the Cursor
Suppose we have the following records:

Notice that multiple records share the exact same timestamp.
This commonly occurs during:
- Bulk API operations
- Data Loader imports
- Batch Apex executions
- ETL processes
- Enterprise integrations
If we sort only by CreatedDate, the ordering is not guaranteed.
Instead *ORDER BY CreatedDate, Id* creates a deterministic ordering.
Why CreatedDate Alone Can Fail
Suppose Page 1 ends with:
Id = 001A CreatedDate = 2026–05–30T10:15:23.847Z
Your next-page query is:
SELECT Id, Name, CreatedDate
FROM Account
WHERE CreatedDate > :cursorDate
ORDER BY CreatedDate
LIMIT 50
The following records would be skipped: (refer the records screenshot above)
- 001B
- 001C
because their CreatedDate equals the cursor value rather than being greater than it. This results in missing records.
The Correct Cursor Pattern
To load the Next Page use below code
SELECT Id, Name, CreatedDate
FROM Account
WHERE (
CreatedDate > :cursorDate
OR (
CreatedDate = :cursorDate
AND Id > :cursorId
)
)
ORDER BY CreatedDate ASC, Id ASC
LIMIT :pageSize
Salesforce can jump directly to the next position without scanning thousands of skipped rows.
To load the Previous Page, use the below code
SELECT Id, Name, CreatedDate
FROM Account
WHERE (
CreatedDate < :cursorDate
OR (
CreatedDate = :cursorDate
AND Id < :cursorId
)
)
ORDER BY CreatedDate DESC, Id DESC
LIMIT :pageSize
Previous-page navigation requires reversing the comparison and sort order.
Since the results arrive in reverse order, they must be reversed again in Apex before being returned to the client.
Performance Comparison

Final Thoughts
Cursor-based pagination is one of the most valuable Salesforce performance patterns available to developers building enterprise applications.
The key insight is not simply replacing OFFSET.
The real goal is to create a stable, deterministic cursor using:
CreatedDate + Id
CreatedDate already provides millisecond precision.
The additional Id ensures that records sharing the same timestamp are never skipped or duplicated.
Together, these fields create a scalable cursor capable of navigating millions of records while avoiding the performance limitations and hard boundaries imposed by OFFSET-based pagination.
For enterprise Salesforce applications, APIs, integrations, and Lightning Web Components, cursor-based pagination should be the default approach rather than an optimization added later.
메타데이터
- post_id
- 72cd3f4b0091
- slug
- apex-cursor-based-pagination-in-salesforce-a-production-ready-guide-with-next-and-previous-page-72cd3f4b0091
- url
- https://medium.com/@harshvikram16/apex-cursor-based-pagination-in-salesforce-a-production-ready-guide-with-next-and-previous-page-72cd3f4b0091
- canonical_url
- https://medium.com/@harshvikram16/apex-cursor-based-pagination-in-salesforce-a-production-ready-guide-with-next-and-previous-page-72cd3f4b0091
- author_url
- https://medium.com/@harshvikram16
- status
- ok
- fetched_at
- 2026-07-14 07:37:53