Optimiza APIs Flask con solicitudes concurrentes usando asyncio y httpx

  Planteamiento del problema

Imagina que tu API necesita hacer varias llamadas a servicios externos que tardan mucho (varios segundos o minutos) y las hace una tras otra, lo que aumenta el tiempo total de respuesta y afecta la experiencia del usuario.

  Objetivo

Mejorar el rendimiento realizando esas llamadas de forma concurrente, es decir, en paralelo, para reducir el tiempo total de espera.

  Tecnologías usadas

  • Python 3.7+
  • asyncio: librería estándar de Python para programación asíncrona.
  • httpx: cliente HTTP async que permite hacer solicitudes concurrentes fácilmente.
  • Flask 2.0+ que soporta vistas async.

Solucionando el problema con asyncio y httpx

Acontinuación, un ejemplo de Flask que utiliza httpx para realizar solicitudes concurrentes y mide el tiempo total de respuesta.

python
from flask import Flask, jsonify
import asyncio
import httpx
import time

app = Flask(__name__)

# Simula llamada a una API externa lenta (por ejemplo, 5 segundos)
async def llamada_lenta_api(ciudad):
  print(f"Consultando API externa para {ciudad}...")
  await asyncio.sleep(5)  # simula retraso IO
  return { "ciudad": ciudad, "datos": f"Info para {ciudad}" }

# Llama concurrentemente a varias APIs (simuladas)
async def obtener_datos_concurrentes(ciudades):
  tareas = [llamada_lenta_api(ciudad) for ciudad in ciudades]
  resultados = await asyncio.gather(*tareas)
  return resultados

@app.route("/datos_concurrentes")
async def datos_concurrentes():
  ciudades = ["bogota", "medellin", "cali", "barranquilla"]
  inicio = time.time()
  resultados = await obtener_datos_concurrentes(ciudades)
  fin = time.time()
  return {
      "tiempo_total_segundos": round(fin - inicio, 2),
      "resultados": resultados
  }

if __name__ == "__main__":
  app.run(debug=True)
  Explicación paso a paso

  1. Funciones async llamada_lenta_api es una función async que simula una llamada lenta con await asyncio.sleep(5). Esto representa la espera por I/O (llamada externa real).

  2. Concurrencia con asyncio.gather En obtener_datos_concurrentes, generamos tareas para cada ciudad y las ejecutamos todas a la vez con asyncio.gather. Esto permite que las esperas se solapen y no se ejecuten una tras otra.

  3. Ruta async en Flask Flask 2.0+ permite definir rutas async (async def). En /datos_concurrentes, llamamos a la función concurrente y medimos el tiempo total.

  4. Tiempo total Aunque cada llamada tarda 5 segundos, todas las llamadas concurrentes tardan aproximadamente 5 segundos en total, no 20.

Cómo probar

No olvides instalar httpx:

bash
pip install httpx

Luego, puedes probar el ejemplo.

bash
python app.py

Abre tu navegador o usa curl para llamar la ruta:

bash
http://localhost:5000/datos_concurrentes

Deberías ver que el tiempo total ronda los 5 segundos, no la suma (20 segundos).