Explainer

What Is Cursor Pagination?

Cursor pagination walks a large result set one page at a time using a bookmark the server hands you. Here is how it works, how it differs from offset paging, and how LeadOcean search uses it.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

Cursor pagination returns a large result set in pages, using a bookmark from the previous page instead of a page number. This page covers how it works, why APIs prefer it to offset paging, and how to use it on LeadOcean search.

Key takeaways

  • A cursor marks your position in a result set. You send it back to get the next page.
  • Offset paging skips rows by counting. Cursor paging resumes from a position, so it stays fast and stable on deep pages.
  • A cursor is opaque. Never build, edit or store one.
  • In LeadOcean, limit sets the page size, meta.nextCursor is the bookmark, and a null cursor means the walk is over.

What it is

Cursor pagination is a method of splitting a result set into pages where each response carries a token that points to where the next page starts.

You make a first request with no cursor. The server returns a page of rows and a token. You send the token with your next request and get the following page.

The token is usually an encoded copy of the last row's sort position. That is why it is called a cursor: it points at a place in an ordered list. Some developers call the underlying technique keyset pagination.

How it works

A cursor walk is a loop. The client never computes a position. It only passes back what it was given.

  1. Send the first request with your filters and a page size. Leave the cursor out.
  2. Read the page and the token. The response holds the rows plus a field with the cursor for the next page.
  3. Send the same request again with the cursor added. Keep every filter and the page size unchanged.
  4. Repeat until the token comes back empty. An empty token means there are no more pages.

A worked example

Say you search for VPs at a placeholder company list and set the page size to 100. The first response returns 100 rows and a cursor. Call it c1.

You send the same search with c1 and get rows 101 to 200 plus a new cursor, c2. The server does not count 100 rows to find your place. It reads the position encoded in c1 and continues from there.

On the last page you get fewer than 100 rows and a cursor of null. You stop. If someone adds a new record while you walk, your next page still starts right after the last row you saw.

Cursor pagination vs offset pagination

Offset pagination asks for "rows 5,000 to 5,100" by number. Cursor pagination asks for "the 100 rows after this position." Offset is simpler to build. Cursor is safer on large, changing data.

Offset paginationCursor pagination
Request saysSkip N rows, return MReturn M rows after this token
Jump to page 50 directlyYesNo, you walk page by page
Cost on deep pagesGrows with the offsetStays flat
New rows during the walkCan shift pages, so rows repeat or vanishResumes after the last row you saw
Needs a stable sort orderYesYes, built into the token
Client builds the positionYes, from a numberNo, the token is opaque

The cost difference is real. The PostgreSQL docs say the rows skipped by an OFFSET clause still have to be computed inside the server, so a large offset might be inefficient (checked September 2026). The same page warns that without a unique ORDER BY you get an unpredictable subset of rows.

Cursor pagination trades random access for stability. You cannot jump to the middle. For an API that exports or syncs data, that is the right trade.

When it matters

Cursor paging matters whenever the data is large, ordered and moving. These are four situations where the choice shows up.

Pulling a full list into your own system

A sync job reads every row once. With offset, a row inserted mid-walk shifts the later pages and you read one row twice or miss one. With a cursor, the walk continues from a fixed position.

Deep pages on large tables

Page 1 is cheap on any method. Page 5,000 is not. Offset makes the server skip every earlier row on each call, so latency climbs with depth. A cursor keeps each page about the same cost.

Retrying after a failure

If a request fails, resend it with the same cursor. A position-based token does not shift when rows change, which is what makes a walk safe to retry. On LeadOcean, do not rely on a cursor surviving for long.

Capped walks

APIs often cap how deep one walk can go, to protect the database. A cursor lets the server stop issuing tokens at the cap. You then narrow the filters and start a new walk instead of paging harder.

How LeadOcean handles it

LeadOcean people and company search use cursor pagination. Page size is limit, from 1 to 100, with a default of 25. Each response carries meta.nextCursor. Send it back as cursor with the same filters. When it is null, the walk is over.

Five rules, from the OpenAPI spec (checked September 2026):

  • Pass back exactly the string you were given. An edited, truncated or hand-built cursor is refused with 400 Invalid cursor.
  • A cursor from one search is refused on a different search. Keep the filters the same.
  • Do not store a cursor. It is a position in one walk, not a handle on a result set. Restart the query instead.
  • One search pages through at most 10,000 rows by default. On that page meta.depthCapped is true and nextCursor stays null.
  • Every person or company returned counts one record. Paging is not free.

Size the list first. A search with count=true and limit=1 is a free sizing call, and meta.total is capped at 100,000. If the total is above 10,000, split the search before you page, for example by country or job level.

This loop walks one search and stops at the end or at the depth cap. The filters are placeholders.

python
import os, requests

url = "https://api.leadocean.io/v1/people/search"
headers = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
body = {
    "jobLevel": ["VP"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["CA"],
    "limit": 100,
}

cursor, rows = None, []
while True:
    if cursor:
        body["cursor"] = cursor
    resp = requests.post(url, headers=headers, json=body).json()
    rows += resp["data"]
    meta = resp["meta"]
    cursor = meta.get("nextCursor")
    if not cursor:
        if meta.get("depthCapped"):
            print("Depth cap reached: narrow the filters and run another search.")
        break

print(len(rows), "rows")

If you connect LeadOcean to an editor, the same filters apply. See connect LeadOcean to Cursor and size your TAM in Cursor.

For bulk work, POST /v1/exports takes the same filters and writes a CSV, so you do not page by hand. Free covers 1,000 records, once. Pro is $499 a month. See pricing. For another field-level explainer, read email verification explained.

FAQ

Is cursor pagination the same as keyset pagination?

Close. Keyset pagination is the technique: resume from the last row's sort key. A cursor is how an API exposes it, as an opaque token that wraps that key. Many APIs use the two words for the same thing.

Why can I not jump to page 10?

A cursor only knows the position after the page you just read. To reach page 10 you walk pages 1 to 9 first. That is the cost of stable, flat-cost paging. If you need random access, narrow the filters instead.

Can I decode a cursor and change it?

No. LeadOcean cursors are signed, and one that has been edited or hand-built is refused with 400 Invalid cursor. Treat the token as a sealed string and pass it back unchanged.

What happens when I hit the end of the results?

meta.nextCursor comes back null and the walk is over. If the search reached its depth cap of 10,000 rows, meta.depthCapped is also true. Narrow the filters and start a new search for the rest.

Does a cursor expire?

LeadOcean gives no lifetime for a cursor and says not to store one. Cursors issued before 2026-09-19 no longer verify. If a cursor is refused, restart the search.

Size your search for free, then page through it with a cursor

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →