Cursor Pagination en desarrollo móvil — qué es, principio y realización

Autor: IT Sectr Publicado: 2026-03-11 Tiempo de lectura: 9 min

Cursor Pagination es un método de carga paginada de datos que utiliza un cursor único para navegar por un conjunto ordenado de registros. Según la Especificación GraphQL (2025), la paginación basada en cursor es el estándar recomendado para APIs que trabajan con datos dinámicos. Cursor pagination elimina los principales inconvenientes del enfoque Offset: inestabilidad durante las inserciones y degradación del rendimiento en grandes desplazamientos.

Puntos clave

  • Cursor Pagination es un método de paginación donde cada registro tiene un identificador cursor único para la navegación.
  • Cursor es un marcador de posición único en un conjunto de datos (normalmente ID, UUID, timestamp) que no cambia con las inserciones.
  • Estabilidad — los nuevos registros añadidos entre solicitudes no desplazan el cursor, eliminando duplicados y saltos.
  • Rendimiento — la consulta WHERE id > cursor utiliza eficientemente el índice sin perder velocidad en grandes conjuntos de datos.
  • Limitación — la paginación por cursor no admite la navegación por número de página (no se puede saltar a la página 5).

¿Qué es Cursor Pagination?

Cursor Pagination es un método de carga paginada donde el servidor devuelve junto con los datos un puntero especial — un cursor. El cliente usa este cursor en la siguiente solicitud para obtener el siguiente lote de registros. El cursor es un identificador único del último elemento de la página actual.

A diferencia de la paginación Offset, donde el cliente dice “dame la página 5 con 20 registros,” la paginación por cursor funciona de otra manera: “dame 20 registros después del registro con ID = 83.” El servidor ejecuta una consulta con WHERE id > 83 y LIMIT 20. Este enfoque garantiza que cada registro caiga exactamente en una página independientemente de las inserciones.

El concepto de paginación por cursor ganó gran aceptación gracias a la especificación Relay Connection (GraphQL), que convirtió la paginación basada en cursor en el estándar para APIs modernas. Relay define el formato de respuesta: edges (array de registros con cursores), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Historia

La paginación por cursor no es una técnica nueva — se usaba en bases de datos mucho antes de la web. En SQL se llama keyset pagination o seek method. El método se popularizó en APIs tras la publicación de la especificación Relay en 2015, que formalizó el formato del cursor como una cadena codificada en base64 para uniformidad en la transmisión HTTP.

Cómo funciona la paginación por cursor

El principio básico de la paginación por cursor es que la consulta usa una condición WHERE sobre un campo indexado para el posicionamiento, no un desplazamiento. Para dirección hacia adelante se usa WHERE id > last_id; para dirección hacia atrás, WHERE id < first_id. El índice B-tree encuentra el primer registro después del cursor en O(log n), proporcionando un tiempo de respuesta estable.

sql
-- Obtener 20 registros después del cursor '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Obtener 20 registros ANTES del cursor '83' (atrás)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Formato del cursor

Un cursor puede ser simple (un valor ID) o complejo (compuesto por varios campos). Los cursores simples son la clave primaria de un registro, por ejemplo, id autoincremental o UUID. Los cursores compuestos se usan para ordenar por campos no únicos, por ejemplo (created_at, id), donde id garantiza la unicidad cuando las marcas de tiempo son idénticas.

Un formato típico de API es un cursor como cadena codificada en base64. El servidor decodifica el cursor, extrae el valor y construye la consulta SQL. La codificación Base64 oculta la estructura interna del cursor al cliente y permite cambiar el formato sin romper la compatibilidad hacia atrás. El cliente recibe los cursores en el campo endCursor de la respuesta y los pasa como cadena en la siguiente solicitud.

Navegación hacia adelante y hacia atrás

La paginación por cursor admite navegación bidireccional. Para el movimiento hacia adelante (next), se usa el cursor del último elemento de la página actual; para hacia atrás (previous), el cursor del primer elemento. Los parámetros after y before en la solicitud determinan la dirección: after toma registros después del cursor, before toma registros antes del cursor.

Cursor vs Offset Pagination

La elección entre paginación por cursor y Offset es una de las decisiones arquitectónicas clave al diseñar una API. Cada método tiene fortalezas y debilidades que determinan su aplicabilidad. Cursor pagination gana en escenarios con datos dinámicos; Offset gana en escenarios con navegación arbitraria.

CaracterísticaCursorOffset
Estabilidad en insercionesAlta (sin duplicados)Baja (desplazamiento de páginas)
Rendimiento en grandes conjuntosO(log n) — estableO(n) — degrada con el crecimiento
Navegación por número de páginaNoSí (page=5)
Complejidad de implementaciónMediaBaja
Soporte RESTcursor/before/afterpage/offset
Soporte GraphQLEstándar RelayNo recomendado

Por qué Offset falla a escala

La paginación Offset realiza un escaneo completo de la tabla hasta la posición OFFSET. Con offset=100000, la base de datos lee y salta 100000 filas, incluso si LIMIT es 20. MySQL y PostgreSQL no pueden optimizar OFFSET — es una característica de implementación de LIMIT/OFFSET en SQL. La paginación por cursor usa un índice B-tree que encuentra la posición en O(log n).

Un problema adicional de Offset es el “salteo” de registros al paginar hacia atrás. Si un usuario cargó la página 5 y en ese momento se añadieron nuevos registros, al solicitar la página 6 verá de nuevo el registro de la página 5 o se perderá los nuevos. Cursor pagination elimina completamente este escenario: el cursor apunta a un lugar específico en el conjunto y las inserciones no cambian la posición.

Implementación de Cursor Pagination

Veamos la implementación de la paginación por cursor en el backend (Kotlin + Spring) y en el cliente (Android + Retrofit). El servidor acepta los parámetros after, before, limit y devuelve una lista de registros con cursores y pageInfo. Una respuesta típica contiene hasNextPage y hasPreviousPage para gestionar la UI de paginación.

kotlin
@GetMapping("/posts")
fun getPosts(
    @RequestParam after: Long?,
    @RequestParam(defaultValue = "20") limit: Int
): CursorResponse<Post> {
    val cursor = after ?: Long.MAX_VALUE
    val posts = repository.findByIdLessThanOrderByIdDesc(
        cursor, PageRequest.of(0, limit)
    )
    val endCursor = posts.lastOrNull()?.id
    return CursorResponse(
        data = posts,
        pageInfo = PageInfo(
            hasNextPage = posts.size == limit,
            endCursor = endCursor
        )
    )
}

Implementación del cliente en Android

En el cliente, la paginación por cursor se implementa mediante PagingSource de Paging 3, donde la clave es un cursor (Long). PagingSource.load recibe LoadParams.key — el cursor del último registro cargado. LoadResult.Page devuelve los datos y nextKey — el cursor para la siguiente página. Cuando nextKey = null, la paginación está completa.

kotlin
// Retrofit API
interface PostApi {
    @GET("posts")
    suspend fun getPosts(
        @Query("after") after: Long?,
        @Query("limit") limit: Int = 20
    ): CursorResponse<Post>
}

// PagingSource with cursor key
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> = try {
        val response = api.getPosts(
            after = params.key,
            limit = params.loadSize
        )
        val nextKey = response.pageInfo.endCursor
        LoadResult.Page(
            data = response.data,
            prevKey = null,
            nextKey = nextKey
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

Implementación GraphQL mediante Relay

En GraphQL, la paginación por cursor se implementa mediante el patrón Relay Connection. Cada tipo tiene una Connection (con pageInfo y edges) y un Edge (node + cursor). La consulta pasa los parámetros first, after, last, before. El servidor devuelve un array de edges con cursores y pageInfo con hasNextPage/hasPreviousPage.

Cuándo usar Cursor Pagination

Se recomienda la paginación por cursor para APIs que trabajan con datos dinámicos donde los registros se añaden o eliminan con frecuencia. Ejemplos clásicos: un feed de noticias en una red social, mensajes de chat, historial de transacciones, comentarios de publicaciones. En todos estos escenarios, la consistencia y la ausencia de duplicados son importantes.

  • Chats y mensajería — cada nuevo mensaje se añade al inicio de la lista. La paginación Offset se desordena con cada nuevo mensaje.
  • Redes sociales y feeds — las publicaciones se publican continuamente. La paginación por cursor garantiza que el usuario no se pierda ni una sola publicación.
  • Historial de pedidos y transacciones — los datos cambian con menos frecuencia, pero la consistencia es crítica para los informes financieros.
  • APIs con grandes volúmenes de datos — millones de registros. Cursor pagination mantiene el rendimiento donde Offset empieza a ralentizarse.
  • APIs GraphQL — el estándar Relay exige la paginación basada en cursor para cumplir con la especificación.
  • Aplicaciones móviles con scroll infinito — el usuario se desplaza hacia abajo cargando nuevos lotes. El enfoque de cursor proporciona una UX fluida sin duplicados.

Cuándo no es adecuada la paginación por cursor

Existen escenarios donde la paginación Offset es más conveniente: paneles de administración donde se necesita navegación por número de página; búsqueda con paginación donde los resultados pueden cambiar; informes y análisis donde se necesita un enlace fijo a la página 5. En estos casos, las ventajas del cursor no superan la complejidad de implementación.

La paginación por cursor no admite “saltar” a una página arbitraria — el usuario no puede hacer clic en “Página 5” e ir allí. Esta es una limitación arquitectónica: contar el número total de páginas requiere una consulta COUNT separada, que puede ser costosa para tablas grandes. En tales casos, un enfoque híbrido: cursor para datos + count para paginación.

Preguntas frecuentes

¿Qué es un cursor en Cursor Pagination?

Un cursor es un identificador único de registro que señala una posición en el conjunto de datos. Puede ser simple (un ID de registro) o compuesto (varios campos). El cliente recibe el cursor del último registro de la página y lo pasa en la siguiente solicitud para obtener el siguiente lote.

¿Por qué Cursor Pagination es mejor que Offset?

Cursor pagination no está sujeto a desplazamiento cuando se añaden nuevos registros — cada elemento cae exactamente en una página. También mantiene la velocidad en grandes volúmenes usando índices en lugar de escanear las primeras n filas. Offset es más simple pero inestable para datos dinámicos.

¿Se puede implementar la paginación por cursor sin GraphQL?

Sí, la paginación por cursor no está vinculada a GraphQL. Se puede implementar en cualquier API REST pasando el cursor como parámetro de consulta ?after=83&limit=20. La respuesta debe contener pageInfo con endCursor y hasNextPage — esto permite al cliente gestionar la carga sin conocer la estructura interna del cursor.

¿Qué cursor usar — ID, UUID o timestamp?

ID autoincremental es la opción óptima: aumenta monótonamente, no cambia, se indexa eficientemente. UUID v7 (ordenado por tiempo) también funciona. Los timestamps pueden producir duplicados en el mismo momento, así que combínalo con ID: (created_at, id) para garantizar la unicidad del cursor.

¿Cómo obtener el número total de páginas con la paginación por cursor?

La paginación por cursor no proporciona el número total de páginas — esta es su limitación. Si necesitas información total, ejecuta una consulta COUNT separada con los mismos filtros. Para tablas grandes, usa un conteo aproximado mediante EXPLAIN o un total almacenado en caché desde analíticas.

Resumen

  • Cursor Pagination es un método de paginación con navegación por identificador único de registro en lugar de desplazamiento.
  • El cursor garantiza la estabilidad del conjunto en inserciones: los nuevos registros no desplazan las páginas ya cargadas.
  • El rendimiento en grandes volúmenes de datos sigue siendo alto (O(log n)) gracias al uso del índice B-tree.
  • Cursor pagination es adecuado para datos dinámicos: chats, feeds de noticias, transacciones, comentarios.
  • La limitación principal es la falta de navegación por número de página y la imposibilidad de saltar a una página arbitraria.
  • La implementación usa WHERE id después del cursor, parámetros after/before y pageInfo en la respuesta.
  • Estándar — Relay Connection GraphQL, pero las APIs REST con parámetros de cursor también están ampliamente extendidas.

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también