Data APIs: cómo gestionar la paginación cuando el volumen desborda lo esperado
Data APIs: cómo gestionar la paginación cuando el volumen desborda lo esperado
El pipeline funciona. Los tests pasan. El entorno de staging no da errores. Y entonces llegas a producción y el volumen real triplica lo que estimaste. La paginación que diseñaste para diez mil registros al día tiene que manejar treinta mil. Y el sistema empieza a perder datos sin que nadie lo note.
Este escenario es más común de lo que los equipos admiten. El problema no es el volumen en sí: es que la mayoría de las implementaciones de paginación asumen un mundo estático que las APIs de datos reales nunca garantizan. Registros que aparecen entre dos llamadas consecutivas, índices que se desplazan, ventanas temporales que se solapan. El resultado es siempre el mismo: pérdida silenciosa de señales.
Entender por qué ocurre —y cómo prevenirlo— es trabajo de arquitectura, no de debugging reactivo.
El problema raíz: offset pagination en un dataset vivo
La paginación por offset (page=1, page=2, page=N) es la implementación más extendida porque es la más simple de razonar. También es la más peligrosa cuando el dataset subyacente muta entre llamadas.
El mecanismo es conocido: si entre la petición de la página 3 y la de la página 4 se insertan nuevos registros al principio del índice, todos los registros se desplazan una posición. El resultado es que la página 4 repite el último registro de la página 3, o salta el primero de la 4. La API no lanza error. El pipeline no levanta alarma. El dato simplemente no llega.
En APIs de señales de medios —donde los registros se indexan en tiempo real y el orden cronológico es el criterio dominante— este problema ocurre en cada ventana de alto volumen. No es una excepción. Es el comportamiento por defecto del sistema bajo carga.
Cursor-based pagination: el estándar que resuelve el problema
La alternativa robusta es la paginación por cursor. En lugar de pedir "la página N", pides "los registros a partir del identificador X". El servidor devuelve un cursor opaco —un token, un timestamp, un ID de registro— que señala el punto exacto desde el que continuar la siguiente llamada.
La ventaja es estructural: el cursor ancla la posición en el dataset con independencia de los registros que se inserten o eliminen después. No hay desplazamiento posible porque no hay aritmética de offsets.
Un patrón de implementación en Python:
import requests
def fetch_all(endpoint, params, token):
cursor = None
results = []
while True:
if cursor:
params["cursor"] = cursor
response = requests.get(
endpoint,
params=params,
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
data = response.json()
results.extend(data.get("items", []))
cursor = data.get("next_cursor")
if not cursor:
break
return results
Este patrón tiene tres propiedades críticas: termina cuando el servidor dice que no hay más datos, no asume nada sobre el tamaño total del dataset, y no pierde registros si el dataset crece durante la iteración.
Ventanas temporales como capa de seguridad adicional
El cursor resuelve el problema de posición. Pero hay un segundo vector de pérdida: las ventanas temporales mal definidas.
Cuando pagas por volumen —modelo habitual en APIs de datos con pricing pay-as-you-go— existe la tentación de ampliar la ventana de consulta para reducir el número de llamadas. Una sola petición para las últimas 24 horas en lugar de doce peticiones de dos horas cada una. El resultado inmediato es positivo: menos llamadas, menos overhead de red. El riesgo latente es que si esa petición falla, pierdes 24 horas de señales en un solo evento.
El patrón correcto es el opuesto: ventanas cortas, solapadas mínimamente, con idempotencia en la ingesta. Una ventana de 15 minutos con un solapamiento de 2 minutos hacia atrás cuesta más llamadas, pero garantiza que cualquier fallo recuperable no destruye más de 15 minutos de datos. Y el solapamiento permite detectar duplicados en lugar de huecos.
from datetime import datetime, timedelta
def generate_windows(start, end, window_minutes=15, overlap_minutes=2):
windows = []
current = start
while current < end:
window_end = min(current + timedelta(minutes=window_minutes), end)
windows.append((current - timedelta(minutes=overlap_minutes), window_end))
current = window_end
return windows
El solapamiento genera duplicados. Eso es manejable. El hueco no lo es.
Idempotencia en la ingesta: el tercer pilar
Cursor + ventanas cortas resuelven la pérdida en el lado del productor. La idempotencia resuelve el problema en el lado del consumidor.
Cada registro que entra al pipeline debe poder insertarse dos veces sin crear duplicados. Esto implica que el modelo de datos tiene un identificador único por registro —normalmente el ID que devuelve la API— y que la capa de persistencia implementa un INSERT OR IGNORE o un ON CONFLICT DO NOTHING equivalente.
Sin idempotencia, el solapamiento de ventanas produce duplicados en el dataset final. Con idempotencia, el solapamiento produce seguridad sin coste en calidad.
El triángulo cursor + ventanas cortas + idempotencia es el mínimo viable para cualquier pipeline que consuma APIs de datos en producción con volumen no trivial.
Lo que el proveedor no va a decirte
La documentación de una API de datos suele describir los parámetros de paginación. No suele describir el comportamiento del índice bajo carga, el TTL de los cursores, ni qué ocurre si una ventana temporal cae en un momento de reindexación del dataset.
Antes de llevar un pipeline a producción, hay tres preguntas que el equipo debe haber respondido:
- ¿Los cursores caducan? Si el cursor tiene un TTL de 5 minutos y tu pipeline tarda 8 en procesarlo, el siguiente
next_cursorfalla en silencio o lanza un error que hay que manejar explícitamente. - ¿El dataset puede reordenarse entre llamadas? Algunos proveedores reindexan periódicamente. Si ocurre durante una iteración de paginación, la posición del cursor puede quedar inválida.
- ¿Qué devuelve la API cuando no hay más registros? Un array vacío, un cursor nulo, un campo
has_more: false. El contrato debe estar documentado. Si no lo está, hay que probarlo empíricamente antes de que el pipeline asuma un comportamiento.
En APIs como las que expone FeedScale, el contrato de paginación está explícito en la documentación técnica. Aun así, validarlo en staging con volumen sintético antes de producción sigue siendo el paso que más equipos omiten y más equipos acaban lamentando.
La regla práctica que resume todo
Un pipeline que pagina correctamente no es el que no falla. Es el que, cuando falla, sabe exactamente qué datos puede haber perdido y tiene los mecanismos para recuperarlos sin reingesta completa.
Cursor en lugar de offset. Ventanas cortas con solapamiento. Idempotencia en la capa de persistencia. Esos tres elementos no son optimizaciones: son la base sobre la que cualquier integración con una data API puede sobrevivir al volumen real.
El volumen real siempre es mayor de lo que dice la estimación inicial. Eso no es un problema de planificación. Es una constante del sector.