Cómo hacer llamadas paralelas a una API en Python
Cuando un programa necesita llamar a una API decenas o cientos de veces — por ejemplo, para alimentar un dashboard o recoger datos de varios endpoints — hacerlo de una llamada en una se vuelve muy lento. Las llamadas paralelas a una API en Python resuelven este problema: en lugar de esperar a que cada petición termine antes de empezar la siguiente, enviamos varias a la vez y el tiempo total baja drásticamente.
Requisitos previos
- Python 3.8 o superior instalado.
- Saber hacer una petición simple con
requestsohttpx. - Una terminal para instalar paquetes con
pip. - Ganas de experimentar: usaremos la API pública de pruebas
jsonplaceholder.typicode.com.
Paso 1: Entender por qué las llamadas secuenciales son lentas
Imagina que necesitas obtener 20 tareas de una API. Con un bucle normal, cada petición solo empieza cuando termina la anterior. Si cada una tarda unos 0,2 segundos, el total ronda los 4 segundos — y crece a medida que añades más peticiones.
import time, httpx
inicio = time.perf_counter()
with httpx.Client() as cliente:
for i in range(1, 21):
r = cliente.get(f"https://jsonplaceholder.typicode.com/todos/{i}")
r.json()
print(f"Sequencial: {time.perf_counter() - inicio:.2f} s")
El problema no es que tu ordenador esté ocupado: casi siempre está esperando la respuesta de la red. Ese tiempo muerto, en el que no ocurre nada, es justo lo que vamos a aprovechar para adelantar otras peticiones.
Paso 2: Instalar httpx
httpx es un cliente HTTP moderno que funciona de forma síncrona (como requests) y también asíncrona, lo que es ideal para peticiones en paralelo. Instálalo con pip:
pip install httpx
Paso 3: Crear una función asíncrona para una llamada
En código asíncrono usamos async def para definir la función y await para esperar la respuesta sin bloquear el resto del programa. Mientras una petición espera a la red, Python queda libre para atender las demás. Esta función obtiene una tarea y devuelve el JSON.
async def obter(cliente, id):
r = await cliente.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
return r.json()
Fíjate en que la función recibe el cliente como argumento: así todas las llamadas reutilizan la misma conexión, lo que es más rápido que abrir una nueva cada vez.
Paso 4: Ejecutar varias llamadas en paralelo con asyncio.gather
asyncio.gather recibe varias tareas y las ejecuta a la vez, devolviendo los resultados en el mismo orden en que las pediste. Es el corazón de las llamadas paralelas.
import asyncio, time, httpx
async def obter(cliente, id):
r = await cliente.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
return r.json()
async def principal():
inicio = time.perf_counter()
async with httpx.AsyncClient() as cliente:
tarefas = [obter(cliente, i) for i in range(1, 21)]
resultados = await asyncio.gather(*tarefas)
print(f"Paralelo: {time.perf_counter() - inicio:.2f} s")
return resultados
resultados = asyncio.run(principal())
El *tarefas despliega la lista como argumentos individuales. Al ejecutarlo, el tiempo total suele pasar de varios segundos a pocas décimas.
Paso 5: Limitar la concurrencia con un Semaphore
Enviar 500 peticiones a la vez puede sobrecargar la API y provocar el error 429 Too Many Requests. Un Semaphore limita cuántas llamadas ocurren al mismo tiempo, para que seas un buen vecino y evites bloqueos.
import asyncio, httpx
async def obter(cliente, limite, id):
async with limite:
r = await cliente.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
return r.json()
async def principal():
limite = asyncio.Semaphore(5) # no máximo 5 ao mesmo tempo
async with httpx.AsyncClient(timeout=10) as cliente:
tarefas = [obter(cliente, limite, i) for i in range(1, 21)]
return await asyncio.gather(*tarefas)
resultados = asyncio.run(principal())
print(f"Recebidas {len(resultados)} respostas")
Con Semaphore(5), solo cinco peticiones se ejecutan a la vez; las demás esperan su turno de forma automática.
Verificar el resultado
Para confirmar que todo salió bien, compara el tiempo impreso en la versión secuencial (Paso 1) con el de la versión paralela (Paso 4): debería ser bastante menor. Comprueba también que len(resultados) es igual al número de peticiones que hiciste (20) y que ningún resultado viene vacío. Si aparecen errores de red, aumenta el timeout o reduce el valor del Semaphore.
Conclusión
Has aprendido a convertir llamadas lentas, una a una, en peticiones paralelas que aprovechan el tiempo de espera de la red, y a controlar la carga con un Semaphore. El siguiente paso es añadir manejo de errores — por ejemplo, reintentar peticiones fallidas — y recoger resultados a medida que llegan con asyncio.as_completed. ¿Has pensado cuántas peticiones por segundo aguanta tu API antes de devolver 429?