---
schema_version: "1"
id: b9b743d4bc65236ec9690fbda9b90415a1c98cee754735f2502b109b09781abe
title: Apex Cursors
collection: legacydeveloper
version: current
locale: en-us
portal_url: https://docs.salesforce.com/legacydeveloper/current/en-us/apexcode/apex_cursors.md
canonical_url: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_cursors.htm
source_site: developer.salesforce.com
content_type: documentation
content_hash: 3c78abeacd58ca2d756cc454213609da7cb2780c60d589290eedbde4ecf7a18c
description: Use Apex cursors to break up the processing of a SOQL query result into pieces that can be processed within the bounds of a single transaction. Cursors provide you with the ability to work with large query result sets, while not actually returning the entire result set. You can traverse a query result in parts, with the flexibility to navigate forward and back in the result set. Package developers and advanced developers can use cursors to work with high-volume and high-resource processing jobs. Cursors combined with chained queueable Apex jobs are a powerful alternative to batch Apex and address some of Batch Apex’s limitations.
---
# Apex Cursors

Use Apex cursors to break up the processing of a SOQL query result into pieces that can be processed within the bounds of a single transaction. Cursors provide you with the ability to work with large query result sets, while not actually returning the entire result set. You can traverse a query result in parts, with the flexibility to navigate forward and back in the result set. Package developers and advanced developers can use cursors to work with high-volume and high-resource processing jobs. Cursors combined with chained queueable Apex jobs are a powerful alternative to batch Apex and address some of Batch Apex’s limitations.

Apex cursors are stateless and generate results from the offset position that is specified in the Cursor.fetch(integer position, integer count) method. You must track the offsets or positions of the results within your particular processing scenario.

A cursor is created when a SOQL query is executed on a Database.getCursor() or Database.getCursorWithBinds() call. When a Cursor.fetch(integer position, integer count) method is invoked with an offset position and the count of records to fetch, the corresponding rows are returned from the cursor. The maximum number of rows per cursor is 50 million, regardless of whether the operation is synchronous or asynchronous. To get the number of cursor rows returned from the SOQL query, use Cursor.getNumRecords().

![Note](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/images/icon_note.png)

#### Note

Apex Cursors establish a fixed set of resulting record IDs when they’re created. Subsequent record updates after the cursor is established don’t affect the existing set of record IDs. For example, if an object field is updated such that the record no longer meets the initial WHERE clause of the SOQL query, the record ID remains in the result set. Similarly, subsequent changes to record sharing rules don’t alter the result set.

Calling the Cursor.fetch() method counts against the SOQL query limit, and the rows fetched count against the SOQL query row limit. You can make a maximum of 100 Cursor.fetch() calls per transaction.

Apex cursors may throw these new System exceptions: System.FatalCursorException and System.TransientCursorException. Transactions that fail with System.TransientCursorException can be retried.

## Best Practices for Apex Cursors

After you first initialize the cursor, maintain it as an instance variable on your Queueable class. When you enqueue the job, the cursor is serialized along with the job state, so you can reuse the same cursor instance across each transaction in the Queueable chain without incurring additional cursor and cursor row creation costs. An instantiated cursor remains valid throughout its 48-hour lifespan.

In other words, don’t recreate the cursor instance for each transaction in the Queueable chain. If you call Database.getCursor() within the Queueable execute() method, a new cursor is created for the current query results. Using this antipattern rapidly exhausts both the DailyApexCursorLimit and the DailyApexCursorRowsLimit across your rolling 24-hour org allowance.

## Apex Cursor Example

This Apex cursor example demonstrates how to process a high volume of records by combining a standard cursor with a chained Queueable class.

The cursor (locator) and the offset tracking variable (position) are initialized in the class constructor and stored as instance variables. Because Queueable job state is serialized, the same cursor instance and position offset are preserved across every transaction hop. During each execution, the job fetches a manageable chunk of records, processes them, updates the position, and re-enqueues itself until the entire result set is exhausted.

This is a simple example that you can use as a starting-point and adapt to your implementation needs.

public with sharing class QueryChunkingQueueable implements Queueable {
    private Database.Cursor locator;
    private Integer position;

    public QueryChunkingQueueable() {
        locator = Database.getCursor(
            'SELECT Id FROM Contact WHERE LastActivityDate = LAST\_N\_DAYS:400',
            AccessLevel.USER\_MODE);
        position = 0;
    }

    public void execute(QueueableContext ctx) {
        Integer remainingRows = locator.getNumRecords() - position;
        if (remainingRows == 0) {
            return; // Nothing to do
        }

        // Take the minimum of batch size and remaining rows to avoid over-fetching
        Integer fetchSize = Math.min(200, remainingRows);

        List<Contact> scope = locator.fetch(position, 200);

        // do something, like archive or delete the scope list records

        // Increment the position
        position += fetchSize;

        if (position < locator.getNumRecords()) {
            // process the next chunk
            System.enqueueJob(this);
        }
    }
}

## Use Apex Cursors with Data 360 Data Model Objects (Beta)

![Important](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/images/icon_note_important.png)

#### Important

Apex Cursors for Data 360 Data Model Objects is a pilot or beta service that is subject to the Beta Services Terms at [Agreements - Salesforce.com](https://www.salesforce.com/company/legal/agreements/) or a written Unified Pilot Agreement if executed by Customer, and applicable terms in the [Product Terms Directory](https://ptd.salesforce.com/). Use of this pilot or beta service is at the Customer's sole discretion.

You can use Database.getCursor() to create an Apex cursor for a SOQL query against a Data 360 data model object (DMO). Use Cursor.fetch() to retrieve records in chunks and Cursor.getNumRecords() to get the number of records in the result set.

![Warning](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/images/icon_note_warning.png)

#### Warning

Running SOQL queries against DMOs can result in Data Services credits being consumed from your Data 360 subscription. For more information on how usage is billed, see [Data 360 Billable Usage Types](https://help.salesforce.com/s/articleView?id=data.c360_a_data_usage_types.htm&language=en_US).

For example, this code creates an Apex cursor for the Individual\_\_dlm DMO. This is a simple example that you can use as a starting-point and adapt to your implementation needs.

public with sharing class DataChunkProcessor implements Queueable {
    private Database.Cursor cursor;
    private Integer position;

    public DataChunkProcessor() {
        // Initialize cursor with DC SOQL
        this.cursor = Database.getCursor('SELECT Name\_\_c, Email\_\_c FROM Individual\_\_dlm', AccessLevel.USER\_MODE);
        this.position = 0;
    }

    public void execute(QueueableContext context) {
        Integer remainingRows = cursor.getNumRecords() - position;
        if (remainingRows == 0) {
            return; // Nothing to do
        }

        // Take the minimum of batch size and remaining rows to avoid over-fetching
        Integer fetchSize = Math.min(200, remainingRows);

        // Fetch a chunk of 200 records without hitting SOQL query row limits or heap limits
        List<Individual\_\_dlm> records = cursor.fetch(position, 200);

        // Process records
        for (Individual\_\_dlm rec : records) {
            // Business logic
        }
        // Increment position
        this.position += fetchSize;

        // Chain the next job if more records are left
        if (this.position < cursor.getNumRecords()) {
            System.enqueueJob(this);
        }
    }
}

Apex cursors for Data 360 DMOs expire 24 hours after creation. If the cursor expires before processing is complete, a System.InvalidQueryLocatorException is thrown. Otherwise, Apex cursors for Data 360 DMOs have the same transaction and daily limits as standard Apex cursors.

To get transaction limits on Apex cursors for Data 360 DMOs, use these methods in the Limits class.

- Limits.getDataCloudCursorRows and its upper bound method Limits.getLimitDataCloudCursorRows.
- Limits.getDataCloudCursors and its upper bound method Limits.getLimitDataCloudCursors.

## Pagination Cursors

Like a standard Apex cursor, an Apex pagination cursor provides a pointer to a large SOQL query result set. However, an Apex pagination cursor is designed for UI-based pagination, such as multipage record lists.

![Important](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/images/icon_note_important.png)

#### Important

Apex pagination cursors don’t support queries against Data 360 Data model objects.

To create a pagination cursor, call Database.getPaginationCursor() or Database.getPaginationCursorWithBinds() with a SOQL query as an argument. You org can have a maximum of 100,000 rows across all active pagination cursors per transaction, regardless of whether the operation is synchronous or asynchronous. This size limit is lower than that of a regular Apex cursor, as pagination cursors are designed for human-readable data.

However, pagination cursors have a higher instance daily limit than that of regular Apex cursors. Whereas standard cursors are limited to 10,000 instances per org per 24-hour period, pagination cursors are limited to 200,000 instances per org per 24-hour period. This higher instance limit supports many users accessing records lists that rely on smaller pagination cursors.

To retrieve a page of rows from a pagination cursor, call PaginationCursor.fetchPage(integer start, integer pageSize). The _start_ parameter is the zero-based index from which to begin fetching rows, and the _pageSize_ is the maximum number of rows to retrieve for this page. The maximum _pageSize_ value is 2000 rows.

Unlike a standard Apex cursor, a pagination cursor retrieves a complete page of records, where record rows deleted after the creation of the cursor are skipped over by default. This way, the number of rows displayed per page is consistent.

For example, let’s say that you create a standard cursor and a pagination cursor on the same SOQL query, where the result set is 100 rows. After the cursors are created, you delete the first five rows in the set, indexed 0-4. If you then call Cursor.fetch(0, 20), only 15 rows are retrieved—rows indexed 5-19. However, if you call PaginationCursor.fetchPage(0, 20), 20 rows are retrieved—rows indexed 5-24. The fetchPage() method automatically skips over the five deleted records so that a complete page is retrieved.

To manage this handling of deleted records, the fetchPage() method returns a Database.CursorFetchResult object instead of only the list of results. The Database.CursorFetchResult object encapsulates the rows retrieved and information for the next pagination call.

- To retrieve the rows as a list of sObjects, call CursorFetchResult.getRecords().
- To retrieve the number of deleted rows that the cursor skipped in the fetchPage() operation, call CursorFetchResult.getDeletedRows().
- To retrieve the next page of results, first call CursorFetchResult.getNextIndex(), and then use the return value as the _start_ parameter in the next fetchPage() call.
- To determine whether to make subsequent calls to fetchPage(), use the CursorFetchResult.isDone() method. The method returns true if the specified _pageSize_ is reached, which indicates that a full page of results is retrieved. It also returns true if the pagination cursor reaches the end of a result set before the specified _pageSize_ is reached, which indicates that a partial, final page of results is retrieved.

Calling the PaginationCursor.fetchPage() and PaginationCursor.fetchDeleted() methods count against the SOQL query limit, and the rows fetched count against the SOQL query row limit.

Apex pagination cursors throw these System exceptions: System.FatalCursorException and System.TransientCursorException. Transactions that fail with System.TransientCursorException can be retried.

## Cursors and Pagination Cursor Limits

Cursors and pagination cursors are subject to both per-transaction limits and rolling 24-hour org limits. For Apex cursor limits for Data 360 objects, see the Use Apex Cursors with Data 360 Data Model Objects (Beta) section.

**Transaction Limits**

Calling the Cursor.fetch() method counts against the SOQL query limit, and the rows fetched count against the SOQL query row limit. You can make a maximum number of Cursor.fetch() calls per transaction as dictated by system limits.

Calling the PaginationCursor.fetchPage() and PaginationCursor.fetchDeleted() methods count against the SOQL query limit, and the rows fetched count against the SOQL query row limit.

To get Apex transaction limits for standard cursors and pagination cursors, use these methods in the Limits class.

- Limits.getApexCursorRows() and its upper bound Limits.getLimitApexCursorRows() method
- Limits.getApexCursors() and its upper bound Limits.getLimitApexCursors() method
- Limits.getApexPaginationCursors() and its upper bound Limits.getLimitApexPaginationCursors() method
- Limits.getApexPaginationCursorRows() and its upper bound Limits.getLimitApexPaginationCursorRows() method
- Limits.getFetchCallsOnApexCursor() and its upper bound Limits.getLimitFetchCallsOnApexCursor() method. This limit is cumulative across standard cursors and pagination cursors.

To view the current transaction limit allowances for Apex cursors and Apex pagination cursors, see the Per-Transaction Apex Limits section of [Execution Governors and Limits](https://developer.salesforce.com/docs/atlas.en-us.264.0.apexcode.meta/apexcode/apex_gov_limits.htm).

**Rolling 24-hour Org Limits**

Rolling 24-hour limits apply across all cursor transactions in the org. Rather than resetting at midnight, rolling limits are recalculated hourly based on total usage over the preceding 24-hour window.

Salesforce consumes these limits when a cursor is created, not when cursor rows are fetched.

- DailyApexCursorLimit–Maximum number of active Apex cursor instances per 24-hour period
- DailyApexPCursorLimit–Maximum number of active Apex pagination cursor instances per 24-hour
- DailyApexCursorRowsLimit– Maximum cumulative number of cursor rows and pagination cursor rows per 24-hour period. If you create a cursor over an N-row result set, N is subtracted from this limit allowance immediately, regardless of the number of rows you fetch later.

To view the current org limit allowances for Apex cursors and Apex pagination cursors, see the Salesforce Platform Apex Limits section of [Execution Governors and Limits](https://developer.salesforce.com/docs/atlas.en-us.264.0.apexcode.meta/apexcode/apex_gov_limits.htm).

![Tip](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/images/icon_note_tip.png)

#### Tip

Rapid exhaustion of the DailyApexCursorRowsLimit or the DailyApexCursorLimit is typically caused by recreating a cursor on every hop of a chained Queueable job rather than using a single cursor instance across transactions. See the Best Practices for Apex Cursors section.

**Expiration Limits**

Apex cursors and pagination cursors expire 48 hours after creation.

## Apex Cursor and Pagination Cursor Limits Example

This example demonstrates how to programmatically check both per-transaction governor limits and 24-hour org limits for Apex cursors.

- Track transaction-level usage for active cursors, fetched rows, and cumulative fetch calls by using the [System.Limits](https://developer.salesforce.com/docs/atlas.en-us.264.0.apexref.meta/apexref/apex_methods_system_limits.htm) class.
- Query rolling 24-hour org allowances for daily cursor instances and total row counts using [OrgLimits.getMap()](https://developer.salesforce.com/docs/atlas.en-us.264.0.apexref.meta/apexref/apex_class_System_OrgLimits.htm#apex_System_OrgLimits_getMap) and [OrgLimit.getLimit()](https://developer.salesforce.com/docs/atlas.en-us.264.0.apexref.meta/apexref/apex_class_System_OrgLimit.htm#apex_System_OrgLimit_getLimit).

// Create a standard cursor
Database.Cursor cursor = Database.getCursor('SELECT Id, Name FROM Account LIMIT 20', AccessLevel.USER\_MODE);
System.debug('Standard Cursors: ' + Limits.getApexCursors() + '/' + Limits.getLimitApexCursors());
System.debug('Standard Cursor Rows: ' + Limits.getApexCursorRows() + '/' + Limits.getLimitApexCursorRows());

// Fetch records
List<Account> batch1 = cursor.fetch(0, 10);
List<Account> batch2 = cursor.fetch(10, 10);

// Create a pagination cursor
Database.PaginationCursor pagCursor = Database.getPaginationCursor('SELECT Id, Name FROM Account LIMIT 15', AccessLevel.USER\_MODE);
System.debug('Pagination Cursors: ' + Limits.getApexPaginationCursors() + '/' + Limits.getLimitApexPaginationCursors());
System.debug('Pagination Cursor Rows: ' + Limits.getApexPaginationCursorRows() + '/' + Limits.getLimitApexPaginationCursorRows());

// Fetch a page
Database.CursorFetchResult page = pagCursor.fetchPage(0, 5);

// Check shared fetch call limit
System.debug('Fetch Calls: ' + Limits.getFetchCallsOnApexCursor() + '/' + Limits.getLimitFetchCallsOnApexCursor());

// Get daily limits map
Map<String, System.OrgLimit> limitMap = OrgLimits.getMap();

// Standard cursor daily limit
System.OrgLimit dailyCursorLimit = limitMap.get('DailyApexCursorLimit');
System.debug('Daily Cursors: ' + dailyCursorLimit.getValue() + '/' + dailyCursorLimit.getLimit());

// Pagination cursor daily limit
System.OrgLimit dailyPCursorLimit = limitMap.get('DailyApexPCursorLimit');
System.debug('Daily Pagination Cursors: ' + dailyPCursorLimit.getValue() + '/' + dailyPCursorLimit.getLimit());

// Shared daily rows limit
System.OrgLimit dailyRowsLimit = limitMap.get('DailyApexCursorRowsLimit');
System.debug('Daily Cursor Rows: ' + dailyRowsLimit.getValue() + '/' + dailyRowsLimit.getLimit());

#### See Also

- [_Apex Reference Guide:_ Cursor Class](https://developer.salesforce.com/docs/atlas.en-us.264.0.apexref.meta/apexref/apex_class_Database_Cursor.htm "Apex Reference Guide: Cursor Class - HTML (New Window)")
- [_Apex Reference Guide:_ PaginationCursor Class](https://developer.salesforce.com/docs/atlas.en-us.264.0.apexref.meta/apexref/apex_class_Database_PaginationCursor.htm "Apex Reference Guide: PaginationCursor Class - HTML (New Window)")