API pagination techniques for large data sets
An API endpoint that returns every customer, transaction or shipment in one response creates a predictable failure point. Memory use grows, transfer times stretch, and a client may receive a document too large to process reliably. Pagination divides a result set into manageable pages, allowing users and downstream systems to retrieve only the records they need. For large data sets, it is also a safeguard against accidental full-table exports.
Two common models dominate modern interfaces: offset pagination, which counts records to skip, and cursor pagination, which continues from a position in a stable ordering. Both appear behind simple HTTP requests, yet their database cost, consistency behaviour and failure recovery differ substantially. The right choice depends on the collection’s size, how often it changes and whether users need numbered pages or uninterrupted traversal.
A durable API contract specifies page size, ordering, continuation tokens and error responses as product behaviour. It keeps the server in control of expensive queries while giving clients a repeatable way to resume. That foundation supports efficient traversal, predictable metadata and responsible handling of Australian data-protection requirements.
Why pagination becomes an architectural concern
Large responses place pressure on several layers at once. The application must query the database, serialise results, hold them in memory and transmit them across the network. A request for “all orders” can therefore consume server capacity even when the caller intends to display only a screenful. Pagination creates a boundary around each unit of work.
That boundary matters for mobile clients in Sydney and Melbourne, where people commonly check banking, retail and delivery applications while moving between trains, shops and home. A smaller response can complete on a crowded connection without delaying the next interaction. The same benefit applies to Australian businesses syncing orders with couriers, warehouses and accounting systems.
The external contract should define a requested limit, a maximum permitted limit and a continuation mechanism. Responses need a consistent shape for records and pagination metadata, including whether more data is available. An exact total count is optional because calculating it can require another expensive query. Clients should be able to stop when no continuation token is returned, rather than guessing from a partial page.
Offset pagination and its hidden costs
Offset pagination selects a page by counting rows to discard. An endpoint such as GET /orders?limit=50&offset=1000 asks the database for the thousand-and-first through thousand-and-fiftieth matching rows. It is easy to understand, works with familiar page-number controls and permits a client to jump directly to a particular location.
The expense appears as the offset grows. A database may read one thousand rows to return fifty, then ten thousand to return the same number later. An index can reduce this cost when its columns match the sort order, but a mismatch between the index and ORDER BY clause can trigger a separate sort. A query counting every match adds further work on a frequently updated table.
Offset pagination also treats a changing collection as if its positions were fixed. If a new order is inserted before the current position, the next page can repeat a record; a deletion can cause one to be skipped. It is most dependable for stable data sets, immutable exports or pages backed by a consistent snapshot. Deep links to numbered pages can still be supported by mapping them to a stored boundary, without making offsets the only available traversal method.
Cursor pagination for stable, scalable traversal
Cursor pagination, often called keyset or seek pagination, continues after the last item already seen. Instead of saying “skip 5,000 rows”, the next request carries the sort values of the final record. The database can use an indexed range condition to locate the next row, avoiding the discarded rows that make deep offsets expensive.
A reliable cursor requires a deterministic order and a unique tie-breaker. Sorting only by created_at is risky when several records share the same timestamp, because their relative order may change between requests. A compound key such as created_at followed by id gives every record a stable position, provided the query uses the same columns, direction and filter scope for every page.
This model suits event feeds, invoice histories and synchronisation endpoints where missing a record matters more than jumping to page eighty. The trade-off is that clients cannot choose an arbitrary position, and the server must issue and validate continuation tokens. The response remains a normal REST API request, while the underlying database query behaves more like a sequence scan through an index.
Designing useful and trustworthy page tokens
The continuation token is part of the API contract, not a decorative base64 string. A practical token represents the last processed sort key, the filters in effect and enough state to reject use in another context. Signing it prevents accidental or deliberate modification; encrypting it can also conceal details that the client has no reason to see. Tokens should never contain names, email addresses or other personal information.
A response can use a small envelope such as {"data":[...],"pagination":{"next_cursor":"opaque-token","has_more":true}}. When the final page is reached, next_cursor can be null. The has_more flag helps clients distinguish another available page from a completed traversal, while the opaque token avoids coupling them to database identifiers or query syntax.
Include a token expiry when the endpoint relies on a short-lived snapshot. Long-lived tokens can otherwise become invalid after an index rebuild, and a stable token should not outlive the consistency guarantees described in its documentation. Avoid promising a total count unless it is cheap and reliable to calculate; an estimated_total label is more honest when a count comes from a cached or approximate source.
Keeping results consistent while records change
Pagination exposes a fundamental issue in distributed systems: a collection can change between two requests. Inserts, deletes and updates may move records across a page boundary. A cursor based only on creation time remains vulnerable when that value is mutable, while a timestamp alone may not identify one record uniquely. The ordering contract must reflect how the underlying data actually changes.
One approach is to include a snapshot identifier or a monotonic sequence in the token. Every page in that traversal then reads from the same logical dataset, provided the database can retain it for a defined period. Event tables that only receive appends are simpler, because their ordering can remain stable. For mutable business records, the server may need a version column, soft-delete handling or a documented policy for records changed during traversal.
Clients still need sensible recovery behaviour. A request can fail after the server has generated a page but before the client receives it; retrying the same cursor should return the same page under the stated consistency model. Record-level edits should generally be fetched through an endpoint keyed by the record identifier, with the list endpoint used for discovery. This separation makes reconciliation clearer than asking a cursor to carry the full history of every field.
Protecting performance under real traffic
Query design determines whether cursor pagination scales. Use composite indexes that support both filtering and the exact sort order, and check the execution plan on realistic data volumes. Select only the fields the endpoint promises, avoid unnecessary joins and consider partitioning an archive table rather than scanning years of history for the newest page. Caching can help, although changing records limit how long any particular page remains authoritative.
Concurrency needs explicit controls as well. Thousands of clients may resume the same collection during a sale weekend, so the API should enforce request quotas, cap page sizes and return a clear 429 response with a Retry-After header. Retries should include jitter to avoid synchronising another burst, while normal GET requests can be repeated without changing server state. These controls protect the service without asking every consumer to implement its own global throttle.
Measure the signals that reveal pagination problems: response latency, rows scanned, empty pages, token validation failures, cache hit rates and throttled requests. Track page size by endpoint rather than assuming all clients behave alike. During engineer onboarding, the implementation notes can be paired with a short video resource to reinforce the distinction between a skipped row and a cursor boundary.
Applying the design in an Australian context
Australian deployments span dense city networks and regional connections, so response size remains a practical concern beyond major eastern capitals. Data can travel over mobile broadband, fixed wireless and intermittent links in parts of Australia. Modest default page sizes, compressed JSON and a clear continuation workflow give clients useful progress without requiring a large download before anything can be displayed.
The local market also shapes usage patterns. Australian retailers, online marketplaces and logistics firms often connect storefronts to warehouse, courier and accounting platforms. A single order can appear in several systems, making duplicate avoidance important during reconciliation. Cursor-based feeds are particularly valuable when a retailer’s catalogue changes frequently or when a finance system must import every invoice in a defined sequence.
For organisations covered by the Privacy Act 1988, the Australian Privacy Principles require appropriate security and governance controls, including those in APP 11. Pagination metadata can still disclose information, so opaque tokens, redacted logs and least-privilege access matter even when record payloads are encrypted in transit. The API should document retention periods for snapshots and avoid using record totals to reveal sensitive information about individuals.
A dependable API returns bounded, ordered pages with explicit continuation rules, predictable errors and a consistency model clients can rely on. That contract works for a small Australian business as well as a national platform, and it turns a large collection into a sequence of controlled, recoverable requests.