Paginação por Cursor vs. Paginação por Offset: Qual a Melhor para Sua API?

Paginação baseada em cursor versus paginação por offset comparadas: desvio de página, custo de offset profundo, SQL de keyset, exemplos do Stripe e do Slack, e como testar ambas no Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

31 agosto 2026

Paginação por Cursor vs. Paginação por Offset: Qual a Melhor para Sua API?

Apidog para empresas

Implantação local

SSO & RBAC

Conforme SOC 2

Explorar Apidog Enterprise

Todo endpoint de lista eventualmente enfrenta a mesma pergunta: como você divide 2 milhões de pedidos em páginas que um cliente pode percorrer? Escolha a paginação por offset e você terá SQL simples mais números de página que os usuários entendem. Escolha a paginação baseada em cursor e você terá resultados estáveis mais latência consistente em qualquer profundidade, mas abrirá mão de “ir para a página 47.”

A maioria das equipes escolhe o offset porque é o padrão em todo tutorial. Então, a tabela de pedidos atinge alguns milhões de linhas, a página 4.000 começa a expirar, e os usuários relatam ver o mesmo registro duas vezes enquanto rolam. Este guia cobre como ambos os estilos funcionam, onde o offset falha, por que Stripe e Slack usam cursores, e como testar qualquer estilo com requisições encadeadas no Apidog. Ao final, você saberá exatamente qual deles se encaixa no seu endpoint.

Se você quiser o panorama mais amplo primeiro, nosso guia de paginação de API aborda cada estratégia lado a lado. Este artigo aprofunda nas duas que mais importam.

Como funciona a paginação por offset

A paginação por offset mapeia diretamente para SQL. O cliente envia um número de página e um tamanho de página; o servidor os traduz em LIMIT e OFFSET.

SELECT id, customer_id, total_cents, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 25 OFFSET 50;

Essa consulta retorna a página 3 da sua lista de pedidos com 25 linhas por página. A requisição se parece com isto:

GET /v1/orders?page=3&per_page=25

E uma resposta típica:

{
  "data": [
    {
      "id": "ord_8821",
      "customer_id": "cus_1932",
      "total_cents": 4599,
      "created_at": "2026-08-30T14:22:07Z"
    }
  ],
  "page": 3,
  "per_page": 25,
  "total": 1848203,
  "total_pages": 73929
}

O apelo é óbvio. Clientes podem pular para qualquer página. O servidor pode retornar uma contagem total. Qualquer desenvolvedor pode construí-lo em uma tarde. Para uma pequena tabela de administração, esta é a escolha certa, e nosso guia passo a passo para paginação em APIs REST mostra uma construção completa por offset.

Mas o offset apresenta dois problemas estruturais, e nenhum deles aparece no desenvolvimento. Ambos aparecem em produção.

Problema 1: desvio de página

O offset conta linhas a partir do topo do resultado ordenado. Ele não sabe nada sobre quais linhas o cliente já viu. Assim, quando linhas são inseridas ou excluídas entre as requisições, as páginas se deslocam sob o cliente.

Digamos que um usuário carrega a página 1 de pedidos ordenados do mais novo para o mais antigo, linhas de 1 a 25. Enquanto ele lê, 3 novos pedidos chegam. Ele requisita a página 2, que é OFFSET 25. As linhas 23, 24 e 25 da primeira resposta foram agora empurradas para as posições 26 a 28. O usuário as vê novamente. Duplicatas.

A exclusão inverte. Remova 3 linhas da página 1 enquanto o usuário a lê, e OFFSET 25 agora pula 3 linhas que o usuário nunca viu. Perda silenciosa de dados, e ninguém recebe um erro.

Para um relatório mensal que ninguém rola em tempo real, o desvio é inofensivo. Para um feed de atividades, um endpoint de sincronização, ou qualquer coisa que um script percorra página por página enquanto as escritas continuam, o desvio significa registros duplicados ou ausentes. Os consumidores percebem.

Problema 2: offsets profundos escaneiam tudo que ignoram

OFFSET 500000 não teletransporta para a linha 500.001. O banco de dados percorre o índice através de meio milhão de entradas, as descarta, e então retorna suas 25 linhas. O custo cresce linearmente com a profundidade: O(n) onde n é o offset.

Números concretos tornam isso real. Em uma tabela de pedidos Postgres com 2 milhões de linhas e um índice em created_at:

O artigo sobre "no-offset" de Markus Winand no Use The Index, Luke demonstra este custo com planos de consulta e vale a pena ler na íntegra. O padrão em produção é um log de consultas lentas dominado por requisições de alto offset, frequentemente de um crawler que diligentemente percorre cada página da sua API pública. Um cliente, e seu p99 dobra.

Como funciona a paginação baseada em cursor

A paginação baseada em cursor, também chamada de paginação por keyset, elimina o contador de linhas. Em vez de “pular 50 linhas”, o cliente diz “me dê as linhas após este registro específico.” O cursor identifica a última linha que o cliente viu, para que o servidor possa buscar diretamente o próximo lote.

O SQL usa uma comparação de linhas na chave de ordenação em vez de OFFSET:

SELECT id, customer_id, total_cents, created_at
FROM orders
WHERE (created_at, id) < ('2026-08-30T14:22:07Z', 'ord_8821')
ORDER BY created_at DESC, id DESC
LIMIT 25;

Observe a comparação de duas colunas. created_at sozinho não é único; dois pedidos podem cair no mesmo milissegundo, e uma chave de ordenação não única significa que as linhas são puladas ou repetidas nos limites da página. Adicionar id como um desempate torna a ordenação total e a paginação exata. Com um índice composto em (created_at, id), o banco de dados busca diretamente o limite e lê 25 entradas. A página 1 e a página 60.000 custam o mesmo.

A API não deveria expor esses valores brutos, no entanto. Implementações reais codificam a chave de ordenação em um token opaco, geralmente base64:

GET /v1/orders?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNDoyMjowN1oiLCJpZCI6Im9yZF84ODIxIn0

Opacidade é uma decisão de design, não ofuscação por si só. Clientes que não conseguem analisar o cursor não podem construir URLs manualmente, o que o deixa livre para mudar a chave de ordenação, adicionar uma dica de shard, ou trocar motores de armazenamento sem quebrar ninguém. O contrato se torna “devolva o que lhe demos,” nada mais.

A contrapartida: não há página 47. Um cursor só sabe “depois desta linha”, então os clientes avançam (e retrocedem, se você emitir um cursor anterior) uma página por vez. Contagens totais também não vêm de graça; a contagem é uma consulta separada. Para designs onde o próprio conjunto de dados é enorme, nosso guia sobre como projetar paginação de API para milhões de registros aborda o lado da escalabilidade com mais profundidade.

Compromissos em um relance

Dimensão Paginação por offset Paginação baseada em cursor
Pular para página arbitrária Sim, qualquer número de página Não, apenas caminhada sequencial
Contagem total / contagem de páginas Barato de incluir Consulta de contagem separada
Desempenho em páginas profundas O(n), degrada com a profundidade O(1) por página em qualquer profundidade
Estabilidade sob escritas Desvia: duplicatas e lacunas Estável, ancorado a uma linha
Custo de construção Trivial Moderado: codificação, desempates, design de índice
Requisitos de ordenação Qualquer ORDER BY funciona Precisa de uma chave de ordenação única e indexada
Cache de URLs de página Fácil, URLs são previsíveis Mais difícil, cursores variam por caminhada
Complexidade do cliente Baixa Baixa, se o envelope for limpo

Uma sutileza nessa tabela merece ênfase: a paginação por cursor exige uma ordenação determinística. Se seu endpoint permite que os clientes ordenem por uma coluna mutável e não única como status, a lógica do keyset se torna dolorosa rapidamente. O offset tolera ordenação imprecisa; cursores a punem.

Qual você deve escolher?

Combine o estilo com a forma como os dados são consumidos.

Tabelas administrativas e dashboards: offset. Ferramentas internas com alguns milhares de linhas, humanos clicando em números de página, e uma contagem visível de “1.848 resultados”. O desvio não importa, a profundidade permanece rasa, e "ir para a página" é um recurso real. O offset vence no custo de construção.

Feeds de rolagem infinita: cursor. Ninguém pula para a página 47 de um feed. Os usuários sempre carregam “mais”, as escritas acontecem constantemente, e duplicatas são visíveis e embaraçosas. Este é o caso de cursor de manual.

APIs públicas: cursor. Você não controla seus consumidores. Alguém escreverá um loop percorrendo cada página, e com offset, páginas profundas se tornam seu problema às 3 da manhã. Cursores mantêm cada página barata e permitem que você evolua os internos por trás do token opaco. Nosso guia de paginação de API REST cobre as convenções de URL e cabeçalho em detalhes.

Exportações e jobs de sincronização: cursor. Um job em lote que puxa todos os 2 milhões de pedidos precisa de duas garantias: nenhuma linha perdida apesar de escritas concorrentes, e custo fixo por página. Offset não oferece nenhuma. Um cursor também oferece um ponto de retomada gratuito quando o job morre na linha 1.4 milhão.

A regra de ouro honesta: offset para interfaces pequenas, navegadas por humanos e com muita contagem; cursores para qualquer coisa grande, ao vivo ou pública.

Como APIs reais lidam com isso

Stripe é totalmente baseado em cursor. Todo endpoint de lista aceita starting_after (um ID de objeto) e limit, e as respostas incluem has_more. Para buscar a próxima página de cobranças, você passa o ID da última cobrança que recebeu. A documentação de paginação da Stripe mostra o padrão; note que não há contagem total em lugar nenhum, uma omissão deliberada devido ao volume de escritas deles.

A API REST do GitHub ainda expõe page e per_page na maioria dos endpoints, com cabeçalhos Link apontando para as páginas seguintes e últimas. Mas leia atentamente a documentação de paginação do GitHub: ela instrui os clientes a seguir o cabeçalho Link literalmente em vez de construir URLs de página, e endpoints mais novos mudaram para cursores, exatamente porque caminhadas profundas de offset em repositórios massivos eram problemáticas.

Slack migrou sua Web API para paginação por cursor e agora a marca como a abordagem que todos os novos métodos usam. Métodos como conversations.history retornam response_metadata.next_cursor, e uma string de cursor vazia significa que você chegou ao fim, conforme descrito na documentação de paginação do Slack.

Três APIs de alto tráfego, e a direção da viagem é única: em direção aos cursores.

Projetando o envelope de resposta

Uma API de cursor vive ou morre com seu envelope. Mantenha-o simples e previsível:

{
  "data": [
    {
      "id": "ord_8846",
      "customer_id": "cus_2201",
      "total_cents": 12900,
      "created_at": "2026-08-30T16:01:44Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNjowMTo0NFoiLCJpZCI6Im9yZF84ODQ2In0"
}

Quatro regras o tornam sólido:

Testando ambos os estilos no Apidog

Bugs de paginação se escondem nas fronteiras: a última página, a página vazia, o cursor cuja linha âncora foi excluída. Clicar manualmente não os pegará, mas um cenário de teste encadeado sim, e é aqui que o Apidog ganha seu lugar no fluxo de trabalho.

Para endpoints de cursor, construa um cenário de teste com duas etapas:

  1. Chame o endpoint e extraia o cursor. Adicione um pós-processador à primeira requisição com o JSONPath $.next_cursor, e armazene-o em uma variável como nextCursor. O Apidog permite copiar o JSONPath diretamente do painel de resposta; o passo a passo completo está em como definir asserções e extrair variáveis com JSONPath.
  2. Execute o loop da requisição da próxima página. Embrulhe uma segunda requisição em um passo ForEach ou de loop, passe {{nextCursor}} como parâmetro do cursor, reextraia $.next_cursor a cada iteração, e saia quando has_more for falso. Afirme a cada passagem que nenhum id se repete da página anterior e que o tamanho da página nunca excede limit.

Para endpoints de offset, a mesma estrutura se aplica com uma variável de contador: incremente page, afirme que o comprimento de data é igual a per_page até a página final, e afirme que total permanece consistente ao longo da caminhada.

Em seguida, adicione os casos de borda como suas próprias etapas, cada uma com asserções explícitas:

Assim que o cenário passar localmente, execute-o no CI a cada merge. Baixe o Apidog gratuitamente e você pode ter o cenário completo de caminhada por cursor, incluindo loops e asserções, funcionando em menos de meia hora.

FAQ

A paginação por cursor é sempre melhor?

Não. O offset é mais adequado quando os usuários precisam de números de página, totais e acesso aleatório a um conjunto de dados modesto, o que descreve a maioria das ferramentas administrativas internas. Cursores são melhores quando o conjunto de dados é grande, as escritas são frequentes ou a API é pública. O modo de falha é usar offset por padrão para um endpoint de lista pública e descobrir o custo O(n) após o lançamento.

Como obtenho uma contagem total com paginação por cursor?

Execute um SELECT COUNT(*) separado com os mesmos filtros, seja como um endpoint distinto ou um parâmetro de consulta opcional como include_count=true. Armazene-o em cache agressivamente; uma contagem aproximada atualizada a cada minuto satisfaz quase todas as UIs. O Stripe ignora totalmente os totais, o que indica a frequência com que os clientes realmente precisam deles.

Posso oferecer ambos os estilos de paginação em um único endpoint?

Você pode, e o GitHub efetivamente faz isso durante sua transição, mas evite em novas APIs. Dois estilos significam dois conjuntos de casos de borda, duas matrizes de teste e confusão do cliente sobre qual usar. Escolha um por endpoint. Se você estiver projetando o contrato do zero, os padrões em nosso guia de paginação de API REST manterão a nomenclatura dos parâmetros consistente em toda a sua superfície.

O que acontece se a linha âncora do cursor for excluída?

Com a paginação por keyset, nada quebra. A comparação WHERE (created_at, id) < (?, ?) não exige que a linha âncora exista; ela busca a posição limite e continua. Esta é uma vantagem real sobre designs de "cursor como pesquisa de linha", e é exatamente o caso de borda que vale a pena afirmar em seu cenário de teste do Apidog antes que um consumidor o encontre por você.

Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs