---
title: "Api pagination"
url: "https://trailassembly.com/salesforce-api-pagination/"
topic: "Data and Integration"
published: "2026-09-20T12:37:55+00:00"
updated: "2026-10-06T13:59:43+00:00"
---

# Api pagination

**Briefly:** the REST query endpoint returns up to 2,000 records and a `nextRecordsUrl` you follow until `done` is true. SOAP does the same through `queryMore` with a locator. That mechanism is fine for reading a result set once, straight through. It is a poor foundation for anything that pauses, restarts, or runs for hours, and for those you want keyset pagination on the record ID instead.

## The built-in mechanism

A query response looks like this:

```apex
{
  "totalSize": 14231,
  "done": false,
  "nextRecordsUrl": "/services/data/v63.0/query/01gRAAAYAAABZoAAA-2000",
  "records": [ ... ]
}
```

Follow `nextRecordsUrl` verbatim. Do not construct it yourself, and do not assume the offset suffix means anything you can manipulate. Each fetch is another API call against your daily allocation.

The page size defaults to the maximum of 2,000 and you can lower it with a request header:

```apex
Sforce-Query-Options: batchSize=500
```

Lowering it is occasionally useful when rows are wide and the response is large enough to cause trouble at your end, or when a query with many formula fields is timing out. Salesforce may also reduce the size on its own for queries with expensive fields, so treat the number as a request rather than a guarantee.

## Why locators are fragile

A query locator is server-side state. It expires, you can only hold a limited number open per user at once, and it represents a snapshot the server is maintaining for you. Three consequences follow.

You cannot pause. If your consumer takes twenty minutes to process a page, the locator may be gone by the time you ask for the next one, and there is no way to resume from where you stopped. You have to start again.

You cannot parallelise. The pages come in sequence, one at a time, because each one hands you the key to the next.

You cannot easily run several extracts at once from the same user without tripping over the open cursor limit, which is a surprisingly low number.

## OFFSET is not the answer

SOQL supports `OFFSET` up to 2,000 rows and no further, which rules it out for any real extract immediately. Even inside that range it is the wrong tool: the database still has to find and discard the skipped rows, so page 10 costs more than page 1, and if records are inserted or deleted while you page, rows shift between pages and you both miss and duplicate records.

It is fine for a user interface showing page 3 of 20. It is wrong for everything else.

## Keyset pagination

The pattern that holds up is to order by ID and remember the last one you saw.

```apex
SELECT Id, Name, SystemModstamp
FROM Account
WHERE Id > '001xx000003DGbTAAW'
ORDER BY Id ASC
LIMIT 2000
```

Salesforce IDs sort consistently, the primary key index makes the filter cheap regardless of how far in you are, and the only state you carry is a single string you can write to a database. Your job can stop, crash, redeploy and pick up exactly where it left off.

Add your real filter alongside it. For an incremental sync, combine a modstamp window with the ID as a tie-break, so records sharing a timestamp still order deterministically:

```apex
WHERE SystemModstamp >= :since
  AND (SystemModstamp > :lastStamp
       OR (SystemModstamp = :lastStamp AND Id > :lastId))
ORDER BY SystemModstamp ASC, Id ASC
```

It is more code than following a locator. It is also the difference between a sync you can restart and one you cannot.

## Bulk query pages differently

Bulk API 2.0 query results are retrieved with a `locator` query parameter whose value comes from the `Sforce-Locator` response header, along with a `maxRecords` parameter controlling chunk size. When the header comes back null, you have everything. It behaves like the REST locator but over much larger blocks, and it is the right mechanism for the volumes where paging 2,000 at a time stops being sensible.

## Frequently asked

**Is totalSize always accurate?** It is the count at the time the query ran. For a long extract, treat it as an estimate rather than a target.

**Does following nextRecordsUrl cost an API call?** Yes. Every page is a request. This is the main argument for larger pages, or for Bulk.

**How do I paginate in Apex?** You generally do not. A SOQL for loop chunks at 200 automatically, and Batch Apex with a query locator handles anything larger.

---

Source: https://trailassembly.com/salesforce-api-pagination/
Trail Assembly — independent Salesforce write-ups.
