Log Entry
Apex Cursors
I have been playing with Apex cursors now that they are generally available, and they solve a practical problem: handling large SOQL result sets without forcing everything into one rigid processing pattern.
Before this, the choice was often:
- batch Apex everywhere,
- custom query loops with manual paging,
- or brittle retry logic when async runs failed halfway.
Cursors give me a cleaner model. I can fetch in chunks, keep a clear checkpoint, and continue predictably.
I use two cursor APIs for two different outcomes:
Database.Cursorwhen I need async, chunked backend processing.Database.PaginationCursorwhen I need predictable list pages in the UI.
Standard cursor: when I use it, and why
I use Database.Cursor when the workload is too large or too uneven for a one-pass transaction.
What this gives me:
- I can process records in bounded chunks instead of hoping one job run can finish everything.
- I can resume from a known position if the next queueable hop is needed.
- I can reduce duplicate/skip bugs because continuation state is explicit (
position).
Typical use cases for me are cleanup jobs, archival jobs, and high-volume update passes where each record can take different effort.
The cursor gives me the ordered rows. I keep two pieces of state:
locator(the cursor),position(the next row index to read).
public class ContactCleanupWorker implements Queueable {
private Database.Cursor locator;
private Integer position;
private static final Integer CHUNK_SIZE = 200;
public ContactCleanupWorker() {
locator = Database.getCursor(
'SELECT Id, LastActivityDate FROM Contact WHERE LastActivityDate = LAST_N_DAYS:400'
);
position = 0;
}
private ContactCleanupWorker(Database.Cursor locator, Integer position) {
this.locator = locator;
this.position = position;
}
public void execute(QueueableContext ctx) {
List<Contact> scope = locator.fetch(position, CHUNK_SIZE);
if (scope.isEmpty()) {
return;
}
for (Contact c : scope) {
c.Description = 'Processed by queueable cursor';
}
update scope;
Integer nextPosition = position + scope.size();
if (nextPosition < locator.getNumRecords()) {
System.enqueueJob(new ContactCleanupWorker(locator, nextPosition));
}
}
}
What I check in this pattern:
positionis always advanced by exactly the number of rows processed.- the next chunk only runs if the cursor still has rows.
If position is wrong, production gets silent correctness bugs: duplicates or skips that are painful to unwind.
Pagination cursor: when I use it, and why
I use this where the consumer is a list in the UI and users expect page-by-page consistency.
Database.PaginationCursor pagCursor = Database.getPaginationCursor(
'SELECT Id, Name FROM Account ORDER BY CreatedDate DESC'
);
Database.CursorFetchResult page = pagCursor.fetchPage(0, 20);
List<Account> rows = page.getRecords();
Integer next = page.getNextIndex();
while (!page.isDone()) {
page = pagCursor.fetchPage(next, 20);
rows.addAll(page.getRecords());
next = page.getNextIndex();
}
This is useful in UI scenarios because pages stay complete when records are deleted between fetches.
In practical terms, that means the user sees a stable page size instead of gaps.
Which one I pick first
I reach for Database.Cursor first when I need explicit continuation control across queueable jobs.
I reach for Database.PaginationCursor when page behavior is the requirement and queueable orchestration is not.
For flows that need fixed batch lifecycle semantics (start/execute/finish behavior), I still use batch Apex.
Notes
- API version: 66.0+.
- Standard cursor result cap:
50,000,000. - Pagination cursor result cap:
100,000. - Pagination page size max:
2000.