# Pagination

Source: https://neondeerdata.com/docs/api/lookup-tables/pagination/

Read paginated results with cursors and optional parallel segments.

## Read every page

For table lists, row lists, and row queries, continue until `pagination.next_cursor` is `null`. A short or empty page can still have another page.

```json
{
  "data": [],
  "pagination": { "next_cursor": "<opaque cursor>" }
}

```

Send a non-null cursor back unchanged as `cursor`. URL-encode it for GET requests or put it in the JSON body for row queries. Do not construct or decode it.

`limit` sets the requested page size, not a guaranteed number of results. You can change `limit` and returned `fields` between pages. The [quickstart](https://neondeerdata.com/docs/api/quickstart/#read-every-page) includes complete loops.

## Continue a read

A cursor belongs to the key, endpoint, table, filter, and segment settings that produced it. It still works after rotating that key.

Reuse the filter, including the order of conditions and `in` values. Reordering an equivalent filter can invalidate the cursor. Values use the field's normalization, so equivalent domain letter case does not change a filter.

A row-list cursor cannot continue a row query, or vice versa. Keep `segment` and `total_segments` unchanged throughout each read.

Cursors expire. If a request returns `400 invalid_cursor`, restore the original settings or restart without a cursor. Skip rows you already processed when restarting.

## Data can change between pages

Each page reads current data. Rows added, changed, or removed during a read may affect the result. For a consistent result, keep the data and readable fields unchanged while reading. Restarting and removing duplicate IDs does not recover every concurrent change.

## Parallel reads

Use `segment` and `total_segments` to split rows between readers. Each reader has its own cursor.

For example, these readers cover separate parts of the table:

```text
GET /v1/lookup-tables/{table_id}/rows?segment=0&total_segments=2
GET /v1/lookup-tables/{table_id}/rows?segment=1&total_segments=2

```

A POST query uses the same settings in its body:

```json
{
  "filter": { "field": "account_domain", "in": ["acme.example.com", "blue-lake.example.com"] },
  "fields": ["account_domain", "owner_email"],
  "segment": 0,
  "total_segments": 2
}

```

Keep the filter and total segment count the same across readers. When the table and readable fields stay unchanged, the segments do not overlap and together cover all matching rows. No separate session is needed.

Increase parallel readers gradually and check the [rate-limit headers](https://neondeerdata.com/docs/api/rate-limits/). More requests do not guarantee a proportional speed increase. Retry temporary errors with the same cursor.
