Diseño de API · Paginación · REST · Rendimiento · Mejores Prácticas
Paginación con cursor vs paginación con offset: cuándo aplica cada patrón en APIs REST
Una comparación práctica de paginación basada en cursor, offset y keyset para APIs REST: compensaciones, guía de implementación y cuándo elegir cada enfoque para conjuntos de datos en crecimiento.
Publicado:
La respuesta corta
La paginación con offset es adecuada para conjuntos de datos pequeños y estáticos donde los usuarios necesitan saltar a páginas arbitrarias. La paginación con cursor es el valor por defecto correcto para colecciones en crecimiento o actualizadas frecuentemente, porque evita la degradación de rendimiento y los problemas de deriva de datos que introducen los enfoques basados en offset a escala. La paginación con keyset es un punto medio que ofrece navegación estable sin la complejidad de codificación del cursor.
Por qué importa la elección del patrón de paginación
Cuando tu API sirve miles o millones de registros, devolver todo en una sola respuesta no es viable. La paginación divide los resultados en fragmentos manejables, pero el método que elijas afecta directamente el rendimiento de las consultas, la complejidad del cliente y la consistencia de los datos a medida que crece tu conjunto de datos.
Los tres patrones que encontrarás en el diseño de APIs REST son la paginación basada en offset, la basada en cursor y la basada en keyset. Cada una tiene compensaciones distintas.
Paginación basada en offset
La paginación con offset utiliza dos parámetros: un offset (el número de fila inicial) y un límite (cuántas filas devolver). Esto se mapea directamente a las cláusulas LIMIT y OFFSET de SQL que la mayoría de los desarrolladores ya conocen.
Cómo funciona:
GET /api/productos?offset=0&limit=10
GET /api/productos?offset=10&limit=10
GET /api/productos?offset=20&limit=10
Cuándo funciona bien:
- Conjuntos de datos pequeños (cientos a unos pocos miles de filas)
- Datos estáticos o que cambian raramente
- Interfaces administrativas donde saltar a la página 50 es un requisito real
- Implementaciones simples donde el tiempo del desarrollador es el cuello de botella
Cuándo falla:
El problema fundamental es que la base de datos debe escanear y descartar cada fila antes del offset. Si solicitas offset 50.000 con un límite de 10, la base de datos lee 50.010 filas y descarta las primeras 50.000. A medida que crece el offset, el tiempo de consulta crece proporcionalmente. Esto no es una preocupación teórica: es una consecuencia directa de cómo funcionan los escaneos en B-tree y heap en bases de datos relacionales.
Un segundo problema es la deriva de datos. Si se insertan o eliminan filas entre solicitudes paginadas, el usuario ve registros duplicados o huecos. Contentful documentó este problema exacto cuando migraron sus APIs de contenido lejos de la paginación con offset: el contenido agregado o eliminado entre solicitudes de página causaba que los resultados se desplazaran de forma impredecible.
Paginación basada en cursor
La paginación con cursor reemplaza el offset numérico por un valor de cursor, típicamente un token opaco que codifica la posición en el conjunto de resultados. El cliente pasa el cursor de la respuesta anterior para obtener la siguiente página.
Cómo funciona:
GET /api/productos?limit=10
→ la respuesta incluye next_cursor: "eyJpZCI6MTIzNH0="
GET /api/productos?limit=10&cursor=eyJpZCI6MTIzNH0=
→ la respuesta incluye next_cursor: "eyJpZCI6MTIzNX0="
Cuándo funciona bien:
- Conjuntos de datos grandes o en crecimiento
- Datos actualizados frecuentemente o en tiempo real
- Interfaces de desplazamiento infinito
- APIs donde el ordenamiento consistente es más importante que el acceso aleatorio
Cómo implementarla correctamente:
El cursor debe codificar los valores usados en la cláusula ORDER BY, no solo un ID de fila. Un cursor basado solo en un ID autoincremental falla cuando las filas comparten la misma clave de ordenamiento o cuando la columna de orden no es única. Una implementación robusta codifica las columnas de ordenamiento más el ID como desempate:
SELECT * FROM productos
WHERE (fecha_creacion, id) > ('2024-01-15T10:30:00Z', 1234)
ORDER BY fecha_creacion ASC, id ASC
LIMIT 10;
El token del cursor codifica fecha_creacion e id de la última fila. Esto asegura un ordenamiento determinista incluso cuando varias filas comparten la misma marca de tiempo.
La compensación:
La paginación con cursor no soporta acceso aleatorio. No puedes saltar a la página 50. Debes recorrer el cursor secuencialmente. Esta es una decisión de diseño deliberada, no una limitación que debas superar. Si tu caso de uso requiere números de página, la paginación con offset o keyset es más apropiada.
Incluir un conteo total:
Una solicitud común del cliente es mostrar “Página 3 de 47” con paginación por cursor. Esto requiere una consulta COUNT(*) separada, lo que añade latencia. La librería fastapi-pagination aborda esto permitiendo un tipo de página de cursor personalizado que incluye el total, pero viene con el costo de una consulta adicional a la base de datos en cada respuesta. Decide si el beneficio de UX justifica el costo de rendimiento para tu API específica.
Paginación con keyset
La paginación con keyset está estrechamente relacionada con la paginación por cursor pero usa valores de columna explícitos en lugar de un token opaco. El cliente pasa el último valor visto de la columna de ordenamiento directamente.
Cómo funciona:
GET /api/productos?limit=10&after_id=1234
GET /api/productos?limit=10&after_id=1235
Cuándo funciona bien:
- Cuando quieres rendimiento similar al cursor sin tokens opacos
- Cuando la columna de ordenamiento es única o puedes agregar una columna de desempate
- Cuando la depuración y la exploración manual de la API son importantes
La compensación:
La paginación con keyset expone tu modelo de datos al cliente. La columna de ordenamiento se convierte en parte del contrato de la API pública. Si necesitas cambiar el orden de ordenamiento o los nombres de las columnas, rompes a los clientes existentes. La paginación con cursor oculta este detalle detrás de un token codificado, haciendo que los cambios de esquema sean más seguros.
Resumen de comparación
| Aspecto | Offset | Cursor | Keyset |
|---|---|---|---|
| Acceso a página arbitraria | Sí | No | No |
| Rendimiento en offsets profundos | Degrada | Estable | Estable |
| Resistencia a deriva de datos | Pobre | Fuerte | Fuerte |
| Complejidad de implementación | Baja | Media | Baja |
| Resiliencia a cambios de esquema | N/A | Buena | Pobre |
| Soporte de conteo total | Nativo | Requiere consulta extra | Requiere consulta extra |
Cuándo elegir cada patrón
Elige paginación con offset cuando:
- Tu conjunto de datos tiene menos de 10.000 filas y crece lentamente
- Tus consumidores de API necesitan saltar a páginas específicas
- Estás construyendo una herramienta administrativa o un reporte donde se esperan números de página
- La simplicidad de
LIMIT/OFFSETpesa más que el costo de rendimiento
Elige paginación con cursor cuando:
- Tu conjunto de datos excede 10.000 filas o crece continuamente
- Los datos se actualizan frecuentemente y la consistencia entre páginas importa
- Estás construyendo un feed, línea de tiempo o experiencia de desplazamiento infinito
- Quieres evitar la deriva de datos sin exponer columnas de ordenamiento a los clientes
Elige paginación con keyset cuando:
- Quieres paginación estable y performante sin tokens opacos
- Tu columna de ordenamiento es estable y es poco probable que cambie
- La depuración y las pruebas manuales de API son importantes para tu flujo de trabajo
- Estás dispuesto a incorporar la columna de ordenamiento en tu contrato público
Lista de verificación para la implementación
Independientemente del patrón que elijas, estas prácticas aplican:
-
Ordena explícitamente siempre. Nunca confíes en el ordenamiento implícito de la base de datos. Una cláusula
ORDER BYsin índice escaneará toda la tabla. -
Indexa tus columnas de ordenamiento. La paginación con offset en profundidad requiere un índice en la columna de offset. La paginación con cursor y keyset requiere un índice en la clave de ordenamiento más cualquier columna de desempate.
-
Devuelve metadatos de navegación. Incluye
next_cursoro equivalente en cada respuesta. La API REST de GitHub usa encabezadosLinkconrel="next"yrel="prev"para este propósito. Las APIs basadas en cursor deben incluir ambos cursoresnextypreviouscuando corresponda. -
Documenta el formato del cursor. Ya sea opaco o explícito, los clientes necesitan saber cómo extraer y pasar el valor del cursor. Documenta si el cursor es URL-safe, codificado en base64 o un valor crudo.
-
Establece un tamaño máximo de página. Permite que los clientes soliciten un límite, pero aplica un límite superior. Esto evita que una sola solicitud sobrecargue tu base de datos o la memoria de tu cliente.
-
Prueba con escrituras concurrentes. Si tus datos cambian mientras un cliente navega por las páginas, verifica que los resultados sean consistentes. Los enfoques de cursor y keyset manejan esto mejor que offset, pero aún debes validar el comportamiento para tu carga de trabajo específica.
Preguntas frecuentes
¿Puedo combinar paginación con offset y cursor en la misma API?
Sí. Algunas APIs ofrecen paginación con offset para casos simples y paginación con cursor para conjuntos grandes. La API REST de GitHub usa paginación basada en offset con números de página. Si ofreces ambos, documenta qué endpoints usan cuál patrón y haz que la distinción sea clara en tu especificación OpenAPI.
¿Funciona la paginación con cursor con filtrado?
Sí, pero el cursor debe codificar las condiciones de filtro además de la clave de ordenamiento. Un cursor que solo codifica la posición de ordenamiento producirá resultados incorrectos cuando los filtros cambien el conjunto de resultados. Incluye los parámetros de filtro en el token del cursor o requiere que los clientes pasen los mismos filtros con cada solicitud paginada.
¿Qué pasa con la paginación en GraphQL?
El spec de conexiones de Relay de GraphQL usa paginación basada en cursor por diseño. El cursor en GraphQL típicamente codifica el ID del nodo y la posición, haciéndolo compatible con el enfoque de cursor descrito aquí. Si estás construyendo una API GraphQL, sigue el spec de Relay en lugar de inventar un modelo de paginación personalizado.
¿Es la paginación con keyset lo mismo que la paginación con cursor?
Resuelven el mismo problema con contratos diferentes para el cliente. La paginación con keyset expone el valor de orden; la paginación con cursor lo oculta detrás de un token. Desde la perspectiva del rendimiento de la base de datos, son equivalentes cuando se implementan correctamente.
Fuentes
- https://www.merge.dev/blog/cursor-pagination
- https://www.contentful.com/blog/cursor-based-pagination
- https://medium.com/@maryam-bit/offset-vs-cursor-based-pagination-choosing-the-best-approach-2e93702a118b
- https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api
- https://github.com/uriyyo/fastapi-pagination/discussions/960
