Paginación por cursor vs. paginación por desplazamiento: ¿Cuál usar en tu API?

Paginación basada en cursor vs paginación por desplazamiento comparadas: deriva de la página, costo de desplazamiento profundo, SQL de conjunto de claves, ejemplos de Stripe y Slack, y cómo probar ambos en Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

31 August 2026

Paginación por cursor vs. paginación por desplazamiento: ¿Cuál usar en tu API?

Apidog para empresas

Despliegue local

SSO & RBAC

Conforme con SOC 2

Explorar Apidog Enterprise

Cada endpoint de lista eventualmente se enfrenta a la misma pregunta: ¿cómo dividir 2 millones de pedidos en páginas por las que un cliente pueda navegar? Elige la paginación por desplazamiento (offset) y obtendrás SQL simple más números de página que los usuarios entienden. Elige la paginación basada en cursor y obtendrás resultados estables más latencia consistente a cualquier profundidad, pero renuncias a "saltar a la página 47".

La mayoría de los equipos eligen el desplazamiento porque es el predeterminado en cada tutorial. Luego, la tabla de pedidos alcanza unos pocos millones de filas, la página 4.000 comienza a agotarse el tiempo de espera, y los usuarios informan que ven el mismo registro dos veces mientras se desplazan. Esta guía cubre cómo funcionan ambos estilos, dónde falla el desplazamiento, por qué Stripe y Slack implementan cursores, y cómo probar cualquiera de los estilos con solicitudes encadenadas en Apidog. Al final, sabrás exactamente cuál se adapta a tu endpoint.

Si primero quieres una visión más amplia, nuestra guía de paginación de API cubre cada estrategia una al lado de la otra. Este artículo profundiza en las dos que más importan.

Cómo funciona la paginación por desplazamiento (offset)

La paginación por desplazamiento se asigna directamente a SQL. El cliente envía un número de página y un tamaño de página; el servidor los traduce a LIMIT y OFFSET.

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

Esa consulta devuelve la página 3 de tu lista de pedidos con 25 filas por página. La solicitud se ve así:

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

Y una respuesta 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
}

El atractivo es obvio. Los clientes pueden saltar a cualquier página. El servidor puede devolver un recuento total. Cualquier desarrollador puede construirlo en una tarde. Para una tabla de administración pequeña, esta es la opción correcta, y nuestra guía paso a paso de paginación en APIs REST recorre una construcción completa de desplazamiento.

Pero el desplazamiento tiene dos problemas estructurales, y ninguno de ellos aparece en el desarrollo. Ambos aparecen en producción.

Problema 1: Deriva de página

El desplazamiento cuenta filas desde la parte superior del resultado ordenado. No sabe nada sobre qué filas ya vio el cliente. Así que cuando se insertan o eliminan filas entre solicitudes, las páginas se mueven debajo del cliente.

Supongamos que un usuario carga la página 1 de pedidos ordenados de más reciente a más antiguo, filas 1 a 25. Mientras lee, llegan 3 pedidos nuevos. Solicita la página 2, que es OFFSET 25. Las filas 23, 24 y 25 de la primera respuesta ahora han sido empujadas a las posiciones 26 a 28. El usuario las ve de nuevo. Duplicados.

La eliminación lo invierte. Elimina 3 filas de la página 1 mientras el usuario la lee, y OFFSET 25 ahora salta 3 filas que el usuario nunca vio. Pérdida silenciosa de datos, y nadie recibe un error.

Para un informe mensual que nadie desplaza en tiempo real, la deriva es inofensiva. Para un feed de actividad, un endpoint de sincronización, o cualquier cosa que un script recorra página por página mientras continúan las escrituras, la deriva significa registros duplicados o faltantes. Los consumidores lo notan.

Problema 2: Los desplazamientos profundos escanean todo lo que omiten

OFFSET 500000 no se teletransporta a la fila 500.001. La base de datos recorre el índice a través de medio millón de entradas, las descarta y luego devuelve tus 25 filas. El costo crece linealmente con la profundidad: O(n) donde n es el desplazamiento.

Los números concretos lo hacen real. En una tabla de pedidos de Postgres con 2 millones de filas y un índice en created_at:

El escrito de Markus Winand sobre "no-offset" en Use The Index, Luke demuestra este costo con planes de consulta y vale la pena leerlo completo. El patrón en producción es un registro de consultas lentas dominado por solicitudes de alto desplazamiento, a menudo de un rastreador que recorre diligentemente cada página de tu API pública. Un cliente, y tu p99 se duplica.

Cómo funciona la paginación basada en cursor

La paginación basada en cursor, también llamada paginación de conjunto de claves (keyset pagination), elimina el contador de filas. En lugar de "saltar 50 filas", el cliente dice "dame filas después de este registro específico". El cursor identifica la última fila que vio el cliente, por lo que el servidor puede buscar directamente el siguiente lote.

El SQL usa una comparación de filas en la clave de ordenación en lugar 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;

Observa la comparación de dos columnas. Solo created_at no es único; dos pedidos pueden caer en el mismo milisegundo, y una clave de ordenación no única significa que las filas se omiten o se repiten en los límites de la página. Añadir id como desempate hace que el orden sea total y la paginación exacta. Con un índice compuesto en (created_at, id), la base de datos busca directamente el límite y lee 25 entradas. La página 1 y la página 60.000 cuestan lo mismo.

Sin embargo, la API no debería exponer esos valores brutos. Las implementaciones reales codifican la clave de ordenación en un token opaco, generalmente base64:

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

La opacidad es una decisión de diseño, no ofuscación por sí misma. Los clientes que no pueden analizar el cursor no pueden construir URLs a mano, lo que te deja libre para cambiar la clave de ordenación, añadir una sugerencia de fragmentación o cambiar de motor de almacenamiento sin romper nada. El contrato se convierte en "devuélveme lo que te dimos", nada más.

La desventaja: no hay página 47. Un cursor solo sabe "después de esta fila", por lo que los clientes avanzan (y retroceden, si emites un cursor anterior) una página a la vez. Los recuentos totales tampoco se obtienen gratis; contar es una consulta separada. Para diseños donde el conjunto de datos es enorme, nuestra guía sobre diseñar la paginación de API para millones de registros cubre el aspecto de escalado con más detalle.

Ventajas y desventajas de un vistazo

Dimensión Paginación por desplazamiento (Offset) Paginación basada en cursor
Saltar a página arbitraria Sí, cualquier número de página No, solo recorrido secuencial
Recuento total / recuento de páginas Barato de incluir Consulta de recuento separada
Rendimiento de página profunda O(n), degrada con la profundidad O(1) por página a cualquier profundidad
Estabilidad bajo escrituras Deriva: duplicados y huecos Estable, anclado a una fila
Costo de construcción Trivial Moderado: codificación, desempates, diseño de índices
Requisitos de ordenación Cualquier ORDER BY funciona Necesita una clave de ordenación única e indexada
Almacenamiento en caché de URL de página Fácil, las URL son predecibles Más difícil, los cursores varían por recorrido
Complejidad del cliente Baja Baja, si la envoltura es limpia

Una sutileza en esa tabla merece ser enfatizada: la paginación por cursor exige una ordenación determinista. Si tu endpoint permite a los clientes ordenar por una columna mutable y no única como status, la lógica del conjunto de claves se vuelve rápidamente dolorosa. El desplazamiento tolera un ordenamiento descuidado; los cursores lo castigan.

¿Cuál deberías elegir?

Haz coincidir el estilo con la forma en que se consumen los datos.

Tablas de administración y dashboards: offset. Herramientas internas con unos pocos miles de filas, humanos haciendo clic en los números de página y un recuento visible de "1.848 resultados". La deriva no importa, la profundidad se mantiene superficial y la función de "saltar a página" es una característica real. El offset gana en costo de construcción.

Feeds de desplazamiento infinito: cursor. Nadie salta a la página 47 de un feed. Los usuarios solo cargan "más", las escrituras ocurren constantemente y los duplicados son visibles y vergonzosos. Este es el caso de uso de cursor de libro de texto.

APIs públicas: cursor. No controlas a tus consumidores. Alguien escribirá un bucle que recorra cada página, y con el desplazamiento, las páginas profundas se convierten en tu problema a las 3 a.m. Los cursores mantienen cada página barata y te permiten evolucionar los internos detrás del token opaco. Nuestra guía de paginación de API REST cubre en detalle las convenciones de URL y encabezados.

Exportaciones y trabajos de sincronización: cursor. Un trabajo por lotes que extrae los 2 millones de pedidos necesita dos garantías: ninguna fila omitida a pesar de las escrituras concurrentes, y un costo plano por página. El desplazamiento no proporciona ninguna de las dos. Un cursor también te da un punto de reanudación gratuito cuando el trabajo muere en la fila 1.4 millones.

La regla general honesta: offset para interfaces pequeñas, navegadas por humanos y con mucho conteo; cursores para cualquier cosa grande, viva o pública.

Cómo lo manejan las APIs reales

Stripe está completamente basado en cursor. Cada endpoint de lista acepta starting_after (un ID de objeto) y limit, y las respuestas incluyen has_more. Para obtener la siguiente página de cargos, pasas el ID del último cargo que recibiste. La documentación de paginación de Stripe muestra el patrón; ten en cuenta que no hay un recuento total en ninguna parte, una omisión deliberada dado su volumen de escritura.

La API REST de GitHub todavía expone page y per_page en la mayoría de los endpoints, con encabezados Link apuntando a las páginas siguiente y última. Pero lee la documentación de paginación de GitHub detenidamente: instruyen a los clientes a seguir el encabezado Link literalmente en lugar de construir URL de página, y los endpoints más nuevos se han desplazado a cursores, exactamente porque los recorridos de desplazamiento profundos sobre repositorios masivos dolían.

Slack migró su API web a la paginación por cursor y ahora la marca como el enfoque que usan todos los métodos nuevos. Métodos como conversations.history devuelven response_metadata.next_cursor, y una cadena de cursor vacía significa que has llegado al final, como se describe en la documentación de paginación de Slack.

Tres APIs de alto tráfico, y la dirección de viaje es unidireccional: hacia los cursores.

Diseñando la envoltura de respuesta

Una API de cursor vive o muere por su envoltura. Mantéenla aburrida y predecible:

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

Cuatro reglas la hacen sólida:

Probando ambos estilos en Apidog

Los errores de paginación se esconden en los límites: la última página, la página vacía, el cursor cuyo registro de anclaje fue eliminado. El clic manual no los detectará, pero un escenario de prueba encadenado sí, y aquí es donde Apidog se gana su lugar en el flujo de trabajo.

Para endpoints de cursor, construye un escenario de prueba con dos pasos:

  1. Llama al endpoint y extrae el cursor. Añade un post-procesador a la primera solicitud con el JSONPath $.next_cursor, y almacénalo en una variable como nextCursor. Apidog te permite copiar el JSONPath directamente desde el panel de respuesta; el recorrido completo está en cómo establecer aserciones y extraer variables con JSONPath.
  2. Bucle de la solicitud de la página siguiente. Envuelve una segunda solicitud en un paso ForEach o de bucle, pasa {{nextCursor}} como parámetro del cursor, vuelve a extraer $.next_cursor en cada iteración y sal cuando has_more sea falso. Asegúrate en cada pasada de que ningún id se repita de la página anterior y que el tamaño de la página nunca exceda el limit.

Para los endpoints de offset, la misma estructura se aplica con una variable de contador: incrementa page, asegura que la longitud de data sea igual a per_page hasta la página final, y asegura que total se mantenga consistente durante el recorrido.

Luego, agrega los casos límite como pasos propios, cada uno con aserciones explícitas:

Una vez que el escenario pase localmente, ejecútalo en CI en cada fusión. Descarga Apidog gratis y podrás tener el escenario completo de recorrido de cursor, incluyendo bucles y aserciones, funcionando en menos de media hora.

Preguntas frecuentes

¿Es la paginación por cursor siempre mejor?

No. El desplazamiento es la mejor opción cuando los usuarios necesitan números de página, totales y acceso aleatorio a un conjunto de datos modesto, lo que describe la mayoría de las herramientas de administración internas. Los cursores son mejores cuando el conjunto de datos es grande, las escrituras son frecuentes o la API es pública. El modo de fallo es optar por el desplazamiento para un endpoint de lista pública y descubrir el costo O(n) después del lanzamiento.

¿Cómo obtengo un recuento total con la paginación por cursor?

Ejecuta un SELECT COUNT(*) separado con los mismos filtros, ya sea como un endpoint distinto o un parámetro de consulta opcional como include_count=true. Almacénalo agresivamente en caché; un recuento aproximado actualizado cada minuto satisface casi todas las interfaces de usuario. Stripe omite los totales por completo, lo que te indica con qué frecuencia los clientes realmente los necesitan.

¿Puedo ofrecer ambos estilos de paginación en un solo endpoint?

Puedes, y GitHub lo hace efectivamente durante su transición, pero evítalo en nuevas API. Dos estilos significan dos conjuntos de casos límite, dos matrices de prueba y confusión del cliente sobre cuál usar. Elige uno por endpoint. Si estás diseñando el contrato desde cero, los patrones en nuestra guía de paginación de API REST mantendrán la nomenclatura de parámetros consistente en toda tu superficie.

¿Qué sucede si se elimina la fila ancla del cursor?

Con la paginación por conjunto de claves, nada se rompe. La comparación WHERE (created_at, id) < (?, ?) no requiere que la fila ancla exista; busca la posición del límite y continúa. Esta es una ventaja real sobre los diseños de "cursor como búsqueda de fila", y es exactamente el caso límite que vale la pena verificar en tu escenario de prueba de Apidog antes de que un consumidor lo encuentre por ti.

Practica el diseño de API en Apidog

Descubre una forma más fácil de construir y usar APIs