Pagination Best Practices for APIs and UIs in 2026

Al Amin/ Author15 min read
Pagination Best Practices for APIs and UIs in 2026

You've probably seen pagination fail in a way that looked harmless at first. Page one loads quickly, the endpoint returns the expected fields, and the UI moves from one result set to the next. Months later, the dataset grows, writes become more frequent, and a request for a deep page starts timing out while users see duplicate or missing records.

That failure usually isn't caused by a single bad query. It comes from treating pagination as a small interface detail instead of a contract shared by the database, API, client, search crawler, keyboard user, and analytics pipeline. These pagination best practices focus on that complete system, including the trade-offs that become visible only after an application is under real load.

When Pagination Goes Wrong in Production

A marketplace search endpoint once looked perfectly healthy with offset pagination. Its query accepted page and limit, sorted listings by creation time, and returned predictable JSON. As the catalog expanded to 80 million rows, users began reporting that ?page=5000 sometimes took 11 seconds to respond. The endpoint hadn't changed. The data had.

The underlying query still had to walk past every earlier row and discard it before returning the requested slice. The deeper the page, the more work the database repeated. Concurrent inserts and deletes made the problem harder to diagnose because the database evaluated each request against a changing dataset. A listing could move into an earlier page between requests, causing the client to see it twice, while another listing moved out and disappeared from the user's sequence.

Production lesson: A pagination parameter can stay stable while its cost and correctness deteriorate underneath it.

The API symptoms spread into other systems. Mobile clients timed out more often because they tend to operate on unreliable connections and smaller budgets. Crawlers followed deep listing paths and consumed resources on slow responses. Analytics counted fewer available listings because interrupted pagination sessions looked like users had reached the end of inventory. Support teams saw “missing results,” while engineers saw successful HTTP responses.

That's why pagination isn't merely a utility concern. It defines whether a user trusts a search result, whether a client can resume safely, whether a crawler can discover inventory, and whether the database can serve the next request without reprocessing the entire history.

The fix required separating concerns that had been bundled together:

  • Storage access needed a query shape that didn't grow expensive with page depth.
  • Ordering needed a unique, deterministic rule instead of a non-unique timestamp alone.
  • API responses needed a continuation token rather than a raw database position.
  • URLs and controls needed to remain usable for crawlers, keyboards, and assistive technology.
  • Client retries needed to account for data changing between requests.

The rest of the design follows from those constraints. A good implementation doesn't make every pagination style identical. It chooses the right model for the dataset, then preserves consistency and usability at every layer.

Offset vs Cursor Pagination in Real Systems

Offset pagination remains useful. It's easy to explain, easy to test, and supports random access. An administrative table with a modest, mostly stable dataset may benefit from page=12, especially when operators need to jump directly to a known page. It also fits APIs where consumers already depend on page numbers.

The cost appears when the dataset is large or changes frequently. The database still reads and discards rows before the requested page, so work grows with the offset. Guidance from CedarDB on pagination describes this deep-offset behavior and recommends keyset or cursor pagination for deep navigation. Large offsets can also create memory pressure or force inefficient scans.

Cursor pagination, also called keyset pagination, asks for rows relative to the last item received. With a stable sort and an indexed condition, the database can seek to the continuation point rather than repeatedly processing earlier rows. The trade-off is fundamental: cursors are excellent for forward traversal, but they don't naturally provide “jump to page 5000.”

Criterion Offset Pagination Cursor Pagination
Query model Skip a requested number of rows, then return the next slice Start after or before a stable sort position
Deep navigation Cost increases with page depth Usually avoids repeated work on earlier pages
Random page access Straightforward Not a native strength
Concurrent writes Can produce duplicates or missing rows More stable when ordering and cursor values are deterministic
API simplicity Familiar page and limit parameters Requires opaque cursor handling and expiry rules
Best fit Small, stable datasets and operator-facing tables Large, write-heavy feeds, catalogs, and APIs

The decision should follow three questions. How large and volatile is the dataset? A small table with rare writes can tolerate offsets. Does the product require arbitrary page jumps? Reporting and back-office workflows may justify offset pagination. What consistency does the user need? A scrolling inventory or activity feed usually values a reliable sequence over direct access to an arbitrary page.

A technical analysis of offset risks in MySQL also highlights inconsistent reads when ordering isn't monotonic or when concurrent inserts and deletes occur. That makes “simple” offset pagination a poor default for high-write endpoints, even when its first-page performance looks fine.

For a real estate search API, the choice may vary by endpoint. A coordinate search designed for bounded browsing can expose conventional pagination, while a continuously changing feed benefits from a cursor contract. Reviewing a concrete endpoint such as RealtyAPI.io's Zillow coordinate search is useful because it puts pagination in the context of actual search parameters, result ordering, and client expectations.

Designing a Cursor That Survives Scale

A cursor is durable only when it identifies a total ordering. A timestamp by itself usually isn't enough because several records can share the same value. If the API sorts by created_at and two listings have the same timestamp, the cursor needs a unique tiebreaker, typically the primary key.

Consider a descending feed ordered by creation time:

SELECT id, created_at, title, price
FROM listings
WHERE (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT ?;

The pair (created_at, id) gives every row a stable position. The next request supplies the last row's pair, and the database returns records strictly before it. The primary key matters because it prevents ambiguous boundaries when timestamps collide. Without that tiebreaker, a record can be skipped or repeated as the database resolves ties differently.

Keep the token opaque

Don't expose raw sort values as a public contract if you can avoid it. Encode the cursor payload as an opaque Base64 token, and treat its contents as an implementation detail. The server might encode a timestamp, identifier, direction, filter fingerprint, or snapshot marker today, then add validation or change the representation later without forcing every client to understand the schema.

A response envelope can stay simple:

{
  "items": [],
  "next_cursor": "opaque-token",
  "has_more": true
}

has_more tells clients whether another request is useful. next_cursor supplies the continuation point. Avoid returning an offset disguised as a cursor, because that preserves the same scaling problem while making the contract harder to inspect.

Validate the cursor before querying

A production cursor should be signed or otherwise protected against tampering, validated for the expected resource and filter set, and rejected when it has expired or belongs to a different query. If a client changes the sort order or search constraints while reusing a cursor, the server should return a clear client error rather than producing an incoherent sequence.

Cursor rule: Encode the sort key and a unique tiebreaker together. The tiebreaker is what turns a convenient continuation token into a stable boundary.

The index must match the ordering and filtering strategy. If the query sorts by (created_at, id), build and test a composite index that supports that access path. Use EXPLAIN or the database's execution-plan tooling to verify that the cursor condition is being used, rather than assuming the index exists and the planner will choose it.

Server-Side Performance and Query Patterns

The query shape determines whether pagination stays cheap as the result set grows. A large OFFSET asks the database to revisit earlier records for every deep request. A keyset predicate such as WHERE (created_at, id) < (?, ?) lets the engine continue from a known position when the composite index matches the sort.

The difference is visible in execution plans, not just application logs. Measure both paths with EXPLAIN ANALYZE, compare rows examined with rows returned, and inspect whether the database performs heap or table lookups for columns that the index could cover. A database query optimization workflow can help teams make that measurement process repeatable rather than tuning pagination by intuition.

Match indexes to the query

A useful index supports the filters, ordering columns, and frequently selected fields. A covering index can reduce additional row fetches when the database can satisfy the query directly from index pages, but it isn't free. Wider indexes consume storage and make writes more expensive, so measure the read benefit against insert and update costs.

Keep filters deterministic. If users can sort by price, creation time, or relevance, each sort mode needs an explicit tie-breaking rule. A query that orders only by price can produce unstable boundaries when several listings share the same price. Add a unique secondary key and make the cursor encode both values.

Control the payload

Pagination limits database work, but it doesn't automatically limit response cost. Cap the page size on the server, set a conservative default, and reject values that exceed the supported maximum. Return the fields the list view needs, then fetch full details on demand. This prevents a client from turning a listing endpoint into an accidental bulk-export mechanism.

The following comparison is intentionally qualitative. Actual latency depends on the database engine, index design, filters, hardware, and cache state, so benchmark your own workload rather than relying on a universal timing claim.

Pattern Page 1 Latency Deep-page Behavior Index Used Consistency Risk
LIMIT with small OFFSET Often simple and responsive Usually acceptable while the offset remains shallow Ordering and filter index, if available Moderate under concurrent writes
Large OFFSET Can appear healthy Work increases as earlier rows are scanned and discarded May require broad scanning High when rows are inserted or deleted
Keyset on (created_at, id) Responsive when indexed Reuses the indexed continuation boundary Composite ordering index Lower, provided ordering is total and filters stay fixed
Unbounded page size Variable Payload and row processing can become excessive Depends on query Can amplify retries and timeouts

Test with realistic row widths, concurrent writes, cold and warm caches, and clients that retry. Also test the failure path. A pagination endpoint that performs well in a quiet local database can still overload production when several consumers request adjacent pages simultaneously. For endpoint-specific throttling behavior, keep the client contract aligned with the service's documented rate limits.

Caching, Retries, and Resilient API Clients

Caching pagination requires distinguishing a stable entry point from a moving continuation. The first page often receives the most traffic and can use stale-while-revalidate behavior, serving a recent cached response while refreshing it in the background. Cursor-anchored pages are more fragile because new rows can appear before the cursor boundary, so keep their cache lifetime short and avoid assuming that a cached next page represents a permanent snapshot.

Cache keys must include every input that changes the result, including filters, sort direction, locale, authorization scope, and page size. Never let a response for one search query satisfy another query because the key omitted a filter. This is a correctness problem, not just a cache-hit problem.

A diagram illustrating strategies for caching, retries, and building resilient API clients for web applications.

Make retries deliberate

A 429 response should reach the client with a clear recovery path. If the server provides Retry-After, honor it rather than applying a competing delay. Retry only operations that are safe to repeat, and use exponential backoff with full jitter so a fleet of clients doesn't send the same request again at the same moment. Cap the retry loop at five attempts, as specified in this resilience pattern, and stop retrying when the server returns a permanent client error.

A minimal TypeScript outline might look like this:

async function fetchWithRetry(
  request: () => Promise<Response>,
  maxAttempts = 5
): Promise<Response> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await request();

    if (response.ok) return response;
    if (response.status === 429) {
      const retryAfter = response.headers.get("Retry-After");
      const delay = retryAfter
        ? Number(retryAfter) * 1000
        : Math.random() * 2 ** attempt * 250;

      await new Promise(resolve => setTimeout(resolve, delay));
      continue;
    }

    if (response.status >= 400 && response.status < 500) return response;
  }

  throw new Error("Pagination request failed after retries");
}

The cursor idempotency trap appears when a client receives a partial response, retries the same cursor, and the underlying dataset changes before the retry. The client may see a duplicate or miss a row. Encode a snapshot version into the cursor and validate it on each request, or use a stable monotonic identifier such as a ULID where the product's ordering model allows it.

For broader caching and conditional-request planning, Surnex crawl budget optimization provides relevant context around reducing unnecessary retrieval. Keep the API's status and recovery behavior explicit for consumers using the documented status codes.

UI Patterns from Numbers to Infinite Scroll

Pagination controls are part of the product's information architecture. Numbered pages give users a sense of total volume, create stable deep links, and expose a straightforward crawl path for search engines. Google's guidance recommends sequential links, self-referencing canonicals on each page, and avoiding fragment-based page numbers in paginated or incrementally loaded content, as described in its pagination and incremental loading documentation.

Load more sits between numbered pagination and infinite scroll. A user keeps their place, activates a visible control, and can inspect the newly added results without losing the surrounding context. It still needs real URL states and crawlable links if the listing content matters to search discovery.

Infinite scroll is appropriate for a continuous, disposable feed where users rarely need to revisit a specific item. It's a poor default for property searches, product catalogs, archives, and comparison workflows. Users can lose their scroll position, the browser back button may not restore the list state, and assistive technology may not receive a useful announcement when new content arrives.

Research summarized in Google's guidance found no clear satisfaction or content-recall advantage for infinite scrolling over pagination, so “more automatic” shouldn't be treated as synonymous with “better UX.” If you do use it, provide stable URL-based pagination underneath and preserve navigation history as content loads.

UX decision: If users compare, bookmark, share, or revisit results, give them a stable URL and an escape hatch from continuous loading.

Rendering and image strategy also matters. Lazy loading can reduce unnecessary work, but it shouldn't hide essential links or cause the initial listing state to become unusable. The practical relationship between lazy loading and Core Web Vitals is worth considering alongside pagination rather than after the UI has shipped. For autocomplete-driven property discovery, a separate endpoint such as RealtyAPI.io's autocomplete API should have its own loading, cancellation, and keyboard behavior instead of inheriting an infinite-scroll pattern.

Accessibility, SEO, and a Pre-Ship Checklist

A pagination component can return correct records and still fail users. Wrap the controls in a navigation landmark labeled clearly as pagination, use a list of links, mark the current page with aria-current="page", and provide descriptive link text. Accessibility references such as eBay's pagination guidance align these patterns with WCAG 2.2 concerns including keyboard operation, visible focus, name and role, and status communication.

Numeric links alone can be confusing in a screen-reader links list. Guidance on accessible pagination labels recommends adding distinguishing information so the purpose of each link remains clear out of visual context. Previous and Next controls should have explicit accessible names, visible focus states, and an appropriate disabled treatment when no destination exists.

Client-side transitions need equal care. Use real URLs rather than hash fragments, preserve browser history, move focus to the new results heading or announce the updated page state, and ensure that newly loaded content doesn't steal focus unexpectedly. Links are generally the right primitive for URL pagination. A button can trigger an in-place load-more action, but it shouldn't replace crawlable URL navigation when the content needs discovery.

Run these checks before merging

  • Cursor stability: Confirm duplicate sort values don't create skipped or repeated records.
  • Query plans: Compare shallow and deep traversal with EXPLAIN ANALYZE.
  • URL behavior: Verify every indexable page has a stable, crawlable URL and an intentional canonical.
  • Keyboard flow: Tab through Previous, numbered links, Next, and any load-more control without a focus trap.
  • Announcements: Confirm assistive technology receives the new page or result-count state after dynamic updates.
  • Retry safety: Replay requests after timeouts and partial responses, then inspect for duplicates and gaps.
  • Cache isolation: Change filters, sort order, user scope, and page size to ensure responses never cross-contaminate.
  • Boundary handling: Test the first page, final page, empty results, expired cursors, and deleted records.

RealtyAPI.io offers paginated real estate search endpoints and developer documentation for integrating listing data into applications that need structured result traversal. Visit RealtyAPI.io to review the API options, obtain an API key, and test a pagination design against real property search use cases.