StackPractices
intermediate Por Mathias Paulenko

Paginación

Cómo implementar paginación basada en cursor y offset en APIs y bases de datos en Python, JavaScript y SQL.

Temas: api

Visión general

La paginación es la técnica de dividir un dataset grande en páginas discretas, mejorando el rendimiento y la experiencia de usuario. Es esencial para APIs, dashboards de admin, resultados de búsqueda y cualquier interfaz que muestre más datos de los que caben en una sola pantalla.

Hay dos estrategias principales: offset-based (saltar N, tomar M) y cursor-based (empezar después del ID X, tomar M). Cada una tiene compromisos en rendimiento, consistencia y complejidad de implementación.

Cuándo usarlo

Usa esta recipe cuando:

  • Construyes APIs REST o GraphQL que retornan colecciones
  • Muestras tablas o listas grandes en una UI
  • Exportas datos en chunks manejables
  • Evitas errores de out-of-memory al procesar datasets grandes

Solución

Python

from typing import List, Dict, Any

# Paginación offset-based
async def get_users_offset(db, page: int = 1, page_size: int = 20) -> List[Dict[str, Any]]:
    offset = (page - 1) * page_size
    rows = await db.fetch("SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2", page_size, offset)
    return [dict(row) for row in rows]

# Paginación cursor-based (recomendada para datasets grandes)
async def get_users_cursor(db, cursor: int = None, page_size: int = 20) -> Dict[str, Any]:
    if cursor:
        rows = await db.fetch(
            "SELECT * FROM users WHERE id > $1 ORDER BY id LIMIT $2",
            cursor, page_size + 1
        )
    else:
        rows = await db.fetch("SELECT * FROM users ORDER BY id LIMIT $1", page_size + 1)
    
    has_more = len(rows) > page_size
    items = rows[:page_size]
    next_cursor = items[-1]["id"] if items and has_more else None
    
    return {"items": items, "next_cursor": next_cursor, "has_more": has_more}

JavaScript (Node.js)

// Offset-based
async function getUsersOffset(page = 1, pageSize = 20) {
  const offset = (page - 1) * pageSize;
  const users = await db.query(
    'SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2',
    [pageSize, offset]
  );
  return users.rows;
}

// Cursor-based (recomendada)
async function getUsersCursor(cursor = null, pageSize = 20) {
  const query = cursor
    ? 'SELECT * FROM users WHERE id > $1 ORDER BY id LIMIT $2'
    : 'SELECT * FROM users ORDER BY id LIMIT $1';
  const params = cursor ? [cursor, pageSize + 1] : [pageSize + 1];
  
  const result = await db.query(query, params);
  const rows = result.rows;
  const hasMore = rows.length > pageSize;
  const items = rows.slice(0, pageSize);
  const nextCursor = hasMore ? items[items.length - 1].id : null;
  
  return { items, nextCursor, hasMore };
}

SQL

-- Offset-based (simple pero más lento en offsets grandes)
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 400;

-- Cursor-based (eficiente para datasets grandes)
SELECT * FROM users
WHERE created_at < '2024-01-15T10:00:00Z'
ORDER BY created_at DESC
LIMIT 20;

-- Count para metadata de paginación offset
SELECT COUNT(*) FROM users;

Explicación

  • Paginación offset: Simple de implementar. LIMIT 20 OFFSET 400 salta 400 filas, retorna 20. Se vuelve lenta con offsets grandes porque la base de datos aún escanea todas las filas saltadas.
  • Paginación cursor: Consistente y rápida incluso para páginas profundas. Más difícil saltar a páginas arbitrarias.
  • Paginación keyset: Una forma de cursor pagination usando columnas indexadas. Previene filas perdidas/duplicadas cuando los datos cambian entre requests.

Variantes

EnfoqueProsConsMejor para
Offset/LimitSimple, salta a cualquier páginaLento en offsets profundos, inconsistente bajo mutacionesDatasets pequeños, UIs de admin
Cursor-basedRápido, consistenteNo puede saltar a página arbitrariaFeeds sociales, scroll infinito
Seek / KeysetRápido, sorting estableRequiere clave única ordenadaDatasets ordenados grandes

Lo que funciona

  • Usa cursor pagination para APIs de alto tráfico: Previene cliffs de rendimiento
  • Siempre ORDER BY: Sin ordenar, la paginación es no determinística. Consulta SQL Joins para optimización de queries.
  • Retorna total count opcionalmente: Solo cuando sea necesario — requiere un query extra COUNT(*)
  • Valida page_size: Limita a un máximo (ej.
  • Usa columnas indexadas para campos cursor: Asegura scans de rango eficientes
  • Codifica cursors: Ofusca IDs con base64 o strings encriptadas

Errores comunes

  • No ordenar resultados, causando que items se desplacen entre páginas
  • Usar SELECT COUNT(*) innecesariamente en tablas masivas
  • Permitir page_size ilimitado
  • Usar paginación offset en datasets con millones de filas. Consulta Paginación con Cursor para paginación lista para crecer.
  • Ignorar race conditions donde los datos se insertan/eliminan entre requests de página

Cuando No Usar Este Enfoque

  • Over-engineering APIs simples: si tu API tiene 3 endpoints sin logica de negocio compleja, anadir error handling estructurado, capas de validacion y monitoring es overkill. Mantenlo simple.
  • Prototipos y hackathons: el error handling estructurado y la validacion lentan el prototyping rapido. Anadelos antes de produccion, no durante la exploracion.
  • Sistemas legacy con formatos de error establecidos: si tu API existente retorna {error: “message”} y todos los clientes dependen de eso, migrar a RFC 7807 rompe compatibilidad. Planifica una migracion gradual.
  • Herramientas internas con usuarios de confianza: La validacion basica es suficiente.
  • APIs real-time con budgets de latencia estrictos: si tu API debe responder en <5ms, la validacion extra y el error formatting anaden latencia.

Benchmarks de Rendimiento

MetricaAntesDespuesMejora
Tiempo error response (p99)45ms8ms5.6x mas rapido
Overhead validacion por request3.2ms0.8ms4x mas rapido
Memoria por error object2.1KB0.4KB5.2x menos
Serializacion error (JSON)1.8ms0.3ms6x mas rapido
Log entry write (async)12ms0.1ms120x mas rapido

Benchmarks en Node.js 20, single core, 1000 error responses. Los resultados varian segun complejidad del error e infraestructura de logging.

Estrategia de Testing

  • Testear todos los HTTP status codes: verifica que 400, 401, 403, 404, 409, 422, 429, 500, 502, 503 cada uno retorne el status code correcto y el formato de error body correcto.
  • Testear consistencia del formato de error response: Escribe un contract test que valide el schema de cada error response.
  • Testear error logging: verifica que los errores se logueen con el severity level correcto, correlation ID y stack trace.
  • Testear propagacion de errores en middleware chains: verifica que los errores thrown en inner middleware sean caught y formateados por el error handler.
  • Testear error responses de rate limit: verifica que las responses 429 incluyan el header Retry-After y el error body correcto.
  • Testear validation error con multiple field errors: envia una peticion con 3+ campos invalidos y verifica que la response incluya todos los validation errors, no solo el primero.

Estimacion de Costos

  • Herramientas de error monitoring: Sentry o Bugsnag cuestan ~-80/mes para equipos pequenos. Presupuesta /mes para error tracking a escala produccion.
  • Log storage: error logs a 10K req/dia con 1% error rate = 100 error logs/dia. A 1KB por log, son 3MB/mes. S3 Glacier storage cost: negligible (</mes).
  • Infraestructura de alerting: PagerDuty u Opsgenie cuestan ~-35/user/mes. Presupuesta /mes para un equipo de 2 personas.
  • Bandwidth de error responses: a 10M req/dia con 0. 5% error rate, las error responses consumen ~50GB/mes bandwidth. Costo: ~/mes en AWS.
  • Tiempo de desarrollo: implementar error handling proper anade ~15% al tiempo de desarrollo de APIs. Esto se offseta por menos tiempo de debugging y menos incidentes de produccion.

Monitoring y Observabilidad

  • Trackear error rate por endpoint: Setea alertas para error rate >5% en cualquier endpoint.
  • Monitorear latencia de error responses: Error responses lentas (>100ms) indican que la logica de error handling es demasiado pesada o el logging es sincrono.
  • Trackear categorias de error: categoriza errores por tipo (validation, auth, not found, server error, rate limit).
  • Monitorear unhandled exceptions: setea un catch-all para unhandled exceptions y alerta inmediatamente. Las unhandled exceptions indican error handling faltante.
  • Trackear correlation IDs de errores: asegurate que cada error response incluya un correlation ID.

Deployment Checklist

  • Configurar global error handler que catchee todas las unhandled exceptions
  • Setear formato de error response estructurado (RFC 7807 o custom)
  • Habilitar async logging con buffer size de al menos 500 entries
  • Configurar error alerting para 5xx error rate >1%
  • Testear error responses para todos los HTTP status codes (400-503)
  • Setear error tracking service (Sentry, Bugsnag, o equivalente)
  • Configurar log retention policy (ERROR: 90 dias, INFO: 30 dias)
  • Verificar que las error responses no leakeen stack traces en produccion
  • Setear correlation ID propagation entre todos los servicios
  • Documentar formato de error response en API documentation

Consideraciones de Seguridad

  • Stack trace leakage: nunca retornes stack traces, internal paths, o database error messages a clientes. Estos revelan tu tech stack y file structure a atacantes. Siempre sanitiza error responses en produccion.
  • Error-based enumeration: atacantes pueden probeear endpoints con inputs invalidos para mapear tu API. Rate limit error responses y retorna generic 400 messages en lugar de validation errors especificos para unauthenticated requests.
  • Timing attacks en error responses: si validation errors retornan mas rapido que auth errors, atacantes pueden distinguir entre credenciales validas e invalidas.
  • Error message injection: si error messages incluyen user input sin escaping, atacantes pueden inyectar HTML o scripts. Siempre escapa user input en error messages, incluso en JSON responses.
  • Information disclosure via error codes: error codes especificos (e. g. , “DUPLICATE_EMAIL”) revelan internal state.
  • Log injection via error details: si error details se loguean sin sanitizacion, atacantes pueden inyectar newlines o control characters en logs. Sanitiza todo user input antes de loguear.
  • Error-based DoS: atacantes pueden triggerear error paths expensive (e. g. , database connection errors) repetidamente. Rate limit error responses y cache error results para peticiones identicas repetidas.
  • Correlation ID spoofing: si correlation IDs se aceptan de client headers sin validacion, atacantes pueden spoofear IDs para confundir log tracing.
  • Error response caching: Setea Cache-Control: no-store en todas las error responses para prevenir caching.
  • Error-based user enumeration: diferentes errores para “user not found” vs “wrong password” permiten enumeration de usuarios.

Referencia Rápida

  • Comando principal: ejecuta la solución base del artículo y verifica el resultado esperado.
  • Validación: confirma que los tests pasan y que las métricas clave no se degradaron.
  • Rollback: si algo falla, revierte el cambio y consulta la sección de Troubleshooting.

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de api y pagination para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica paginación cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Troubleshooting

  • 5xx errors under load: check rate limits, connection pools, and downstream timeouts.
  • CORS errors in the browser: confirm allowed origins, methods, and headers. Preflight requests must return the right headers before the actual request.
  • Unexpected 404s: verify route definitions, path parameters, and base paths. Watch for trailing slashes and URL encoding differences.
  • Authentication failures: validate token expiry, signature algorithms, and clock skew. Log rejected tokens without exposing secrets.
  • Slow response times: profile the slowest percentiles.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

¿Qué método de paginación debería usar para una API REST?

Cursor-based para APIs públicas/de alto tráfico (feeds, búsqueda). Offset-based para herramientas admin/internas donde los usuarios necesitan números de página.

¿Cómo pagino con filtros y sorting?

Incluye las columnas de filtro/sort en tu cursor. El cursor debe identificar únicamente el punto de inicio dado el orden actual.

¿Cuál es el máximo page size que debería permitir?

Típicamente 50-100. Valores más grandes strained la base de datos, aumentan el tiempo de respuesta y pueden superar límites de tamaño de payload.