Icono del sitio IT&ライフハックブログ|学びと実践のためのアイデア集

Ajuste de rendimiento y estrategia de caché en FastAPI 101: una receta práctica para convertir una API lenta en una “API ligera y rápida”

green snake

Photo by Pixabay on Pexels.com

Ajuste de rendimiento y estrategia de caché en FastAPI 101: una receta práctica para convertir una API lenta en una “API ligera y rápida”


Una vista general rápida (resumen)

  • En la mayoría de los casos, lo que vuelve lenta a FastAPI no es el framework en sí, sino problemas de diseño como consultas a BD, el problema N+1, índices inexistentes o insuficientes y falta de caché.
  • Empieza por hacer bien los endpoints async y por cubrir pooling de conexiones y optimización de consultas en SQLAlchemy 2.x para reducir tiempos de espera desperdiciados.
  • Luego combina cabeceras HTTP de caché (Cache-Control, ETag) con fastapi-cache2, Redis y herramientas relacionadas para cachear respuestas y resultados de funciones, de modo que dejes de repetir trabajo costoso.
  • Apilando “micro-optimizaciones específicas de FastAPI” (número de workers de Uvicorn, elección de respuesta JSON, revisión de middleware, etc.), irás logrando una API más ligera, más rápida y más fácil de operar.
  • Por último, esta guía incluye una hoja de ruta de “por dónde empezar” y explica el impacto para aprendices, equipos pequeños y equipos SaaS/startup.

A quién le conviene leer esto (personas concretas)

1) Desarrolladores solitarios / aprendices

  • Ejecutas una app FastAPI en Heroku, un VPS o una configuración pequeña en la nube.
  • Los usuarios y los datos crecieron un poco, y a veces sientes que las respuestas son “algo lentas”.
  • Piensas: “ya lo hice async, así que debería ser rápido… ¿verdad?”, pero no estás del todo seguro.

Para ti, esta guía se centra en qué sospechar primero y en qué orden mejorar, con pasos prácticos y de baja fricción.

2) Ingenieros backend en un equipo pequeño

  • Un equipo de 3–5 personas construye una API FastAPI + SQLAlchemy y, a medida que se acumulan funciones, la latencia empeora lentamente.
  • Quieres abordar consultas N+1 e índices faltantes, pero no sabes por dónde empezar.
  • Quieres caché, pero te abruman opciones como caché HTTP, Redis y fastapi-cache2.

Para ti, esta guía organiza BD + estrategia de caché en un modelo de tres capas que puedes compartir como base dentro del equipo.

3) Equipos SaaS / startups

  • Ya tienes usuarios/tráfico reales, y la latencia y el throughput en horas pico afectan directamente al negocio.
  • Ya empezaste a usar Redis/CDNs, pero decidir “cuánta caché debe vivir en la capa de API” es difícil.
  • Estás pensando en escalar a múltiples instancias o Kubernetes, y quieres un diseño que no lamentes después.

Para ti, esta guía ayuda a encontrar cuellos de botella, patrones prácticos de SQLAlchemy + caché y una estrategia multi-capa incluyendo caché HTTP.


Auto-chequeo de accesibilidad (legibilidad y claridad)

  • Estructura: primero “cómo pensar los cuellos de botella”, y luego profundización en orden: optimización de BD → estrategia de caché → ajuste específico de FastAPI → hoja de ruta.
  • Terminología: los términos clave (N+1, pooling de conexiones, cabeceras HTTP de caché) incluyen una explicación breve al primer uso.
  • Código: bloques más cortos con comentarios mínimos para reducir el coste de escaneo visual.
  • Público: principalmente lectores “intermedios post-tutorial”, manteniendo secciones legibles de forma independiente.

En general, apunta al tipo de claridad y estructura que esperarías cerca de WCAG AA para escritura técnica.


1) Las causas reales de problemas de rendimiento: empieza cuestionando “¿qué está lento?”

Lo primero que conviene recalcar es que “FastAPI es lento” es más raro de lo que parece.

Los cuellos de botella comunes se ven así:

  • Capa de BD
    • Consultas N+1 (bucleando y disparando un SELECT cada vez)
    • Índices faltantes o insuficientes
    • Traer columnas/tablas innecesarias
  • Capa de red
    • Llamar APIs externas de forma secuencial
    • Estrategia débil de timeout/reintento que hace crecer las esperas en cascada
  • Capa de aplicación
    • Sin caché, por lo que se repite el mismo cálculo costoso
    • Trabajo intensivo de CPU (procesamiento de imágenes, formateo de JSON grande, etc.) atascado en un solo worker

Los ajustes específicos de FastAPI (cambiar clase JSON, ajustar workers) suelen empezar a rendir después de abordar estas causas raíz.


2) Endpoints async y bases de SQLAlchemy para el ajuste

2.1 La “línea que no debes cruzar” con async/await

FastAPI soporta endpoints síncronos (def) y asíncronos (async def).
Pero “hacer todo async” no lo vuelve rápido automáticamente.

  • El trabajo I/O-bound (BD, APIs externas, archivos) se beneficia mucho de async
  • El trabajo CPU-bound (transformaciones de imágenes, matemáticas pesadas) choca con límites por el GIL; hilos/async no lo escalan mágicamente—separar procesos (p. ej., Celery) suele ser mejor

Al escribir endpoints async, confirma siempre que las librerías que llamas también sean compatibles con async. Si llamas una sesión síncrona de SQLAlchemy repetidamente desde async def, las ganancias de throughput pueden ser menores de lo esperado.

2.2 SQLAlchemy 2.x + pooling de conexiones

Abrir una conexión nueva a la BD cada vez puede ser un gran overhead.
Con SQLAlchemy, puedes configurar el pool con create_engine():

# app/infra/db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "postgresql+psycopg://user:pass@localhost:5432/app"

engine = create_engine(
    DATABASE_URL,
    pool_size=20,      # concurrencia base
    max_overflow=0,    # no crecer más allá (ajusta según el entorno)
)

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)

Luego en FastAPI, reutiliza sesiones con una dependencia:

# app/deps/db.py
from app.infra.db import SessionLocal

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Este patrón de “pool + dependencia” es una base sólida para la mayoría de apps FastAPI + SQLAlchemy.

2.3 Consultas N+1 e índices

El problema N+1 es una trampa clásica de ORMs:

  • Ejemplo: iterar usuarios y acceder a .posts, disparando un SELECT extra por usuario
  • Arreglo: eager load con selectinload() / joinedload(), o reescribir con JOINs explícitos

Además, si las columnas usadas en WHERE o JOIN no tienen índices, las consultas pueden volverse muy lentas a medida que crecen las tablas. Revisa planes (EXPLAIN) y agrega los índices correctos.


3) Fundamentos de caché: qué cachear y dónde

Ahora, estrategia de caché.

3.1 Piensa en “tres capas de caché”

La caché se puede dividir por “dónde vive”:

  1. Lado cliente/CDN/proxy (cabeceras HTTP de caché)

    • Navegadores/CDNs reutilizan respuestas
    • Controlado por Cache-Control, ETag, etc.
  2. Lado aplicación (memoria/Redis, etc.)

    • “Cerca” de FastAPI
    • Cachea resultados de funciones o consultas
  3. Lado BD

    • Vistas materializadas o tablas precalculadas para agregados pesados

Esta guía se centra principalmente en (1) y (2).

3.2 Qué NO deberías cachear (o debes tratar con cuidado)

  • Poner datos personales o información específica de sesión en una caché compartida es peligroso
  • Usa Cache-Control: private o incluye claves específicas de usuario (p. ej., user_id) en la capa de app
  • Separa datos “seguros para todos” de datos específicos por usuario

4) Manejo de cabeceras HTTP de caché en FastAPI

Empieza por caché HTTP—suele ser de alto impacto y bajo coste de implementación, excelente como primer paso.

4.1 Añadir Cache-Control

Por ejemplo, un endpoint cuyo ranking puede cachearse 60 segundos:

# app/api/ranking.py
from fastapi import APIRouter, Response

router = APIRouter(prefix="/ranking", tags=["ranking"])

@router.get("")
def get_ranking(response: Response):
    # imagina que esto se calcula desde la BD
    data = {"items": ["A", "B", "C"]}

    # cacheable por 60s (incluyendo proxies intermedios)
    response.headers["Cache-Control"] = "public, max-age=60"
    return data

Ahora navegadores/CDNs pueden reutilizar la respuesta por un corto tiempo.

4.2 ETag y solicitudes condicionales

Patrón común: “Si no cambió, devuelve 304 Not Modified”.

import hashlib
import json
from fastapi import Request, Response

@router.get("/popular")
def get_popular_items(request: Request, response: Response):
    data = {"items": ["A", "B", "C"]}
    body = json.dumps(data, sort_keys=True).encode("utf-8")
    etag = hashlib.sha1(body).hexdigest()

    inm = request.headers.get("if-none-match")
    if inm == etag:
        return Response(status_code=304)

    response.headers["ETag"] = etag
    response.headers["Cache-Control"] = "public, max-age=120"
    return data

En producción, a menudo calcularás ETags a partir de campos de versión o timestamps de actualización en la BD.


5) Añade caché en la aplicación con fastapi-cache2 + Redis

Cuando la caché HTTP no alcanza, añade una caché de capa aplicación con Redis. Ejemplo con fastapi-cache2.

5.1 Instalar

pip install fastapi-cache2 redis

Ejecuta Redis (ejemplo con Docker):

docker run -d --name redis -p 6379:6379 redis:7

5.2 Código de inicialización

# app/core/cache.py
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
import redis.asyncio as redis

async def init_cache():
    client = redis.from_url("redis://localhost:6379/0", encoding="utf8", decode_responses=True)
    FastAPICache.init(
        backend=RedisBackend(client),
        prefix="fastapi-cache:",
    )
# app/main.py
from fastapi import FastAPI
from app.core.cache import init_cache

app = FastAPI(title="FastAPI Cache Example")

@app.on_event("startup")
async def on_startup():
    await init_cache()

5.3 Cachear un endpoint (o función)

El decorador @cache() facilita cachear resultados:

from fastapi import APIRouter
from fastapi_cache.decorator import cache

router = APIRouter(prefix="/articles", tags=["articles"])

@router.get("")
@cache(expire=60)   # caché por 60s
async def list_articles():
    # asume que esto es una consulta pesada a la BD
    return [{"id": 1, "title": "Hello"}, {"id": 2, "title": "World"}]

La primera llamada guarda el resultado en Redis; las siguientes responden rápido desde Redis por 60 segundos.

5.4 Añadir claves de caché específicas por usuario

Puedes personalizar la clave con namespace o key_builder. Útil para caché por usuario:

from fastapi import Depends, Request
from fastapi_cache.decorator import cache
from app.deps.auth import get_current_user

def user_cache_key_builder(func, *args, **kwargs):
    request = kwargs.get("request")
    user = kwargs.get("current_user")
    return f"user:{user.id}:path:{request.url.path}"

@router.get("/me")
@cache(expire=30, key_builder=user_cache_key_builder)
async def get_my_dashboard(
    request: Request,
    current_user=Depends(get_current_user),
):
    # agregado pesado por usuario
    ...

En sistemas reales, cuidado con el manejo de argumentos, colisiones de claves e invalidación.


6) Caché multi-capa y cómo pensar TTL e invalidación

Cachear no es solo “poner caché”. La clave es “¿cuánto tiempo es válido?” y “¿cómo lo invalidamos?”.

6.1 Cómo elegir TTL

  • Los datos cambian a menudo, pero un pequeño retraso es OK
    • TTL corto (segundos a minutos): rankings, tendencias, etc.
  • Los datos casi no cambian
    • TTL más largo (minutos a horas): datos maestros
  • Los datos nunca pueden estar stale
    • no cachear, o usar no-cache (debe revalidar) en caché HTTP

Empieza cacheando “cosas que pueden estar un poco desactualizadas”.

6.2 Patrones de invalidación

  • Dejar expirar naturalmente (por TTL)
  • Borrar en eventos de actualización (p. ej., actualizar un artículo borra solo la caché de ese artículo)
  • Claves versionadas (v1:ranking → cambiar a v2:ranking al actualizar)

Con Redis más complejo, comparte una convención de nombres de claves de caché a nivel equipo para mantenerlo.


7) Puntos de ajuste específicos de FastAPI

Tras arreglos de diseño, las micro-optimizaciones pasan a valer la pena.

7.1 Cantidad de workers de Uvicorn

Puedes correr múltiples procesos con --workers:

uvicorn app.main:app \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4
  • Empieza alrededor de #núcleos CPU a 2× #núcleos CPU
  • Si las cargas son realmente pesadas, considera offloading a Celery o similar en vez de solo subir workers

7.2 Elegir clase de respuesta JSON

Para salidas JSON grandes, cambiar la clase de respuesta puede ayudar:

from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)

orjson es conocido por su velocidad; si la serialización JSON consume CPU, puede valer la pena.

7.3 Cantidad y orden de middlewares

Demasiados middlewares añaden overhead por request.

  • Mantén solo lo que de verdad necesitas
  • Si un middleware es pesado, limítalo a ciertas rutas o a una sub-aplicación

8) Medición y verificación: sigue mejoras con números

El tuning de performance necesita medición para evitar “se siente más rápido”.

  • Local/staging:
    • Pruebas de carga con ab, hey, wrk, etc.
    • Sigue latencia P95/P99, tasa de error y cambios en RPS
  • Producción:
    • Visualiza latencia y throughput con Prometheus + Grafana
    • Idealmente también mide “tasa de aciertos de caché” y “conteo de consultas a BD”

Intenta verificar cambios como: “P95 pasó de X ms a Y ms tras cachear”.


9) Impacto según tipo de lector y por dónde empezar

9.1 Para devs solitarios / aprendices

  • Empieza con cabeceras HTTP de caché (Cache-Control) y pooling de conexiones en SQLAlchemy.
  • Luego prueba fastapi-cache2 en APIs de lectura pesada y siente cuánto trabajo elimina la caché.
  • Incluso con decenas de usuarios, la “sensación de rapidez” y el “margen” del sistema pueden mejorar mucho.

9.2 Para ingenieros en equipos pequeños

  • Para alinearse como equipo, suele ser más fácil hablar en tres pasos:
    1. Arreglar N+1 e índices faltantes
    2. Ajustar bien el pooling de conexiones
    3. Añadir caché (HTTP + Redis)
  • Usa logs/métricas para encontrar el cuello de botella primero y prioriza las mayores ganancias.

9.3 Para equipos SaaS / startups

  • Asume una estrategia multi-capa (HTTP/Redis/BD) y documenta “qué va dónde”.
  • Antes de añadir más complejidad con Redis/CDN, a menudo se logran mejoras enormes con mejor calidad de consultas, diseño de esquema y arreglos de N+1.
  • Luego combina fastapi-cache2 y/o una capa de caché propia para escalar con suavidad.

10) Una hoja de ruta paso a paso (para avanzar gradualmente)

  1. Medir el estado actual

    • Mide latencia y RPS para endpoints representativos
    • Registra conteo de consultas a BD y conteo de llamadas a APIs externas
  2. Revisar BD y consultas

    • Arregla N+1 e índices faltantes
    • Ajusta el pooling de conexiones
  3. Introducir cabeceras HTTP de caché

    • Añade Cache-Control a endpoints de solo lectura que puedan estar ligeramente desactualizados
    • Considera ETag y/o Last-Modified cuando haga falta
  4. Añadir caché de aplicación

    • Cachea endpoints/funciones específicas con Redis + fastapi-cache2
    • Define TTLs, diseño de claves y estrategia de invalidación
  5. Ajuste específico de FastAPI

    • Revisa workers de Uvicorn, clase de respuesta y configuración de middleware
    • Si la CPU es el cuello de botella, considera offloading a Celery o procesos separados
  6. Observación y ajuste continuo en producción

    • Itera: mejorar → medir → mejorar
    • Actualiza la estrategia de caché y el diseño de BD a medida que crece tu producto

Lecturas adicionales (si quieres profundizar)

Nota: reflejan el estado en el momento de escribir; revisa cada fuente para info más reciente.


Cierre

Ajustar rendimiento y diseñar estrategia de caché puede sentirse “difícil” o “difícil de cambiar más tarde”.
Pero en la práctica, incluso pasos pequeños—empezando por endpoints muy usados y datos que puedan estar un poco desactualizados—pueden mejorar drásticamente la velocidad percibida.

  • Primero, encuentra el cuello de botella
  • Luego, arregla problemas de BD/consultas
  • Después, combina caché HTTP y caché Redis para que el trabajo pesado no se repita

Avanzando con estos tres, tu app FastAPI puede convertirse en un “socio” mucho más ligero y confiable.

Prueba un paso a la vez, a tu propio ritmo.
Estaré apoyando discretamente para que tu API siga funcionando sin problemas.


Salir de la versión móvil