Skip to content

Batch Processing using Apex Cursors – A use case of Apex Cursors

Salesforce Apex Cursors is generally available in Spring ’26. Cursors provides the ability to work with large query result sets, while not actually returning the entire result set.

Apex cursors are stateless so it does not provide a finish method as we have in batch apex. It generate results from the offset position and the count of records using the Cursor.fetch(integer position, integer count) method.

With cursors, you can start with a large batch size, then decrease the batch size in case of failure to drop the size down when you need to.

In the following Apex class, I’ve refactored it from Batch Apex to use Apex Cursors for handling a large volume of records efficiently. The class now processes records in chunks of 200 per Queueable transaction. When fewer than 200 records remain, the batch size automatically adjusts to match the remaining count.

This class retrieves all U.S. Contact records and updates the Citizenship_Status__c field to “US Citizen”. When the queueable job completes, the next chunk of records is automatically enqueued for processing until all records are updated.

public class ApexCursorExample implements Queueable {
    
    private Database.Cursor locator;
    private Integer position;
    private static final Integer BATCH_SIZE = 200;

    public ApexCursorExample() {
        locator = Database.getCursor('SELECT Id, Citizenship_Status__c FROM Contact WHERE Citizenship_Status__c = NULL AND MailingCountry = \'United States\' ');
        position = 0;
    }
    // Overloaded constructor to resume progress
    public ApexCursorExample(Database.Cursor loc, Integer pos) {
        locator = loc;
        position = pos;
    }
    public void execute(QueueableContext ctx) {        
        Integer totalRecords = locator.getNumRecords();
        // Compute remaining and fetch size safely
        Integer remaining = totalRecords - position;
        Integer fetchSize = Math.min(BATCH_SIZE, remaining);
        
        // Fetch next chunk
        List<Contact> scope = (List<Contact>)locator.fetch(position, fetchSize);        
        for (Contact c : scope) {
            c.Citizenship_Status__c = 'US Citizen';
        }
        update scope;

        position += scope.size();

        // Queue the next job only if there's more data left
        if (position < totalRecords)
            System.enqueueJob(new ApexCursorExample(locator, position));
    }
}