green snake
Photo by Pixabay on Pexels.com
目次

Procesamiento en segundo plano práctico con FastAPI: Guía de diseño de colas de trabajo con BackgroundTasks y Celery


Resumen (primero la visión general)

  • Para operaciones en las que no quieres que las personas usuarias tengan que esperar (envío de correos, procesamiento de imágenes, generación de informes, etc.), la norma básica es procesarlas en segundo plano, separadas de la respuesta HTTP.
  • BackgroundTasks, incorporado en FastAPI, es adecuado para “ejecutar pequeñas tareas después de enviar la respuesta, dentro del mismo proceso”.
  • En cambio, para trabajos pesados o de larga duración, o cuando necesitas reintentos y gestión de colas, combinar FastAPI con la cola de tareas distribuida Celery te da una base mucho más sólida.
  • En este artículo, integraremos tanto BackgroundTasks como Celery en FastAPI y ordenaremos de forma concreta cuál elegir para cada caso de uso.
  • Finalmente, resumiremos los puntos operativos y patrones relacionados con pruebas, monitorización, reintentos, escalado, etc., y presentaremos una hoja de ruta paso a paso para la implantación.

A quién le será útil (imágenes concretas de las personas lectoras)

  • Desarrolladores individuales y personas que están aprendiendo
    “Después de subir un archivo quiero generar una miniatura, pero quiero devolver la respuesta inmediatamente.” “Cada vez que envío un correo, la respuesta se vuelve lenta…”
    → Aprenderás cómo implementar un procesamiento sencillo en segundo plano con BackgroundTasks y entenderás hasta qué punto es suficiente.

  • Ingenieros/as de backend en equipos pequeños
    Has ido acumulando procesos costosos en tiempo como generación de informes PDF e integraciones con APIs externas, y se está volviendo doloroso tener todo mezclado en el flujo principal del API.
    → Verás cómo pasar a una arquitectura de cola de trabajos + workers combinando Celery con FastAPI.

  • Equipos de desarrollo de SaaS y startups
    Tienes decenas de miles de correos, notificaciones o procesos batch al día, y los reintentos y la monitorización adecuados empiezan a ser esenciales.
    → Entenderás las bases de la arquitectura de Celery, su monitorización (Flower, etc.), el diseño de reintentos y el escalado.


Evaluación de accesibilidad (legibilidad y consideración)

  • Estructura de la información
    El artículo usa una estructura de “pirámide invertida”: primero explica el concepto y las diferencias a grandes rasgos, después entra en BackgroundTasks, luego en Celery y, finalmente, presenta una tabla comparativa y una hoja de ruta.

  • Terminología y tono
    Los términos técnicos se explican brevemente la primera vez que aparecen y luego se usan de manera consistente. El tono es educado y relajado, pero no excesivamente informal.

  • Bloques de código
    Todo el código se muestra en bloques monoespaciados con comentarios concisos. Se añaden líneas en blanco para que la vista no se pierda.

  • Personas lectoras previstas
    Se asume que las personas lectoras han tocado un poco de Python o FastAPI, pero las secciones están estructuradas para poder leerse de forma independiente y progresiva.

En conjunto, está escrito pensando en una accesibilidad aproximadamente equivalente a un nivel AA, de forma que una amplia variedad de lectores pueda seguirlo cómodamente.


1. ¿Por qué necesitamos procesamiento en segundo plano?

Primero, ordenemos el “por qué necesitamos siquiera procesamiento en segundo plano”.

1.1 Peticiones HTTP y el problema del “tiempo de espera”

Normalmente, un endpoint de FastAPI sigue este flujo:

  1. El cliente envía una petición
  2. El servidor la procesa
  3. Cuando termina el procesamiento, devuelve una respuesta

Si se mezcla procesamiento pesado en este flujo…

  • Las personas usuarias se ven obligadas a esperar varios segundos
  • El navegador puede llegar a un timeout de la petición
  • Cuando aumenta el acceso concurrente, los workers se saturan

y se producen problemas similares.

1.2 Tareas adecuadas para el procesamiento en segundo plano

Aquí algunos ejemplos típicos de tareas que son mucho más agradables si se descargan al segundo plano:

  • Envío de correos, notificaciones de Slack, notificaciones push
  • Generación de miniaturas de imágenes, creación de PDFs, transcodificación de vídeo
  • Agregación de grandes archivos CSV/Excel y generación de informes
  • Llamadas repetidas a APIs externas (respetando límites de uso mientras se ejecutan secuencialmente)
  • Importación/exportación de datos a gran escala

En la mayoría de los casos, mientras la persona usuaria reciba rápidamente un resultado del tipo “hemos aceptado tu solicitud”, el resto puede proceder más despacio entre bambalinas.


2. Uso de BackgroundTasks incorporado en FastAPI

Empecemos por BackgroundTasks, que proporciona FastAPI de serie. Es “un mecanismo para ejecutar pequeñas tareas en el mismo proceso después de haberse enviado la respuesta”.

2.1 El ejemplo más simple: notificación por correo

# app/main.py
from fastapi import FastAPI, BackgroundTasks

app = FastAPI(title="BackgroundTasks sample")

def send_email(to: str, subject: str, body: str) -> None:
    # En la práctica, aquí iría la lógica para conectarse al servidor de correo y enviar
    print(f"[MAIL] to={to}, subject={subject}, body={body}")

@app.post("/users/{user_id}/welcome")
async def send_welcome(user_id: int, background_tasks: BackgroundTasks):
    # Omitimos la búsqueda del usuario aquí
    email = f"user{user_id}@example.com"

    # Registrar el envío del correo para después de devolver la respuesta
    background_tasks.add_task(
        send_email,
        to=email,
        subject="¡Bienvenido!",
        body="Gracias por registrarte.",
    )
    return {"status": "ok", "message": "Tu registro ha sido aceptado"}

En este endpoint:

  • La respuesta HTTP se devuelve inmediatamente
  • Después de eso, send_email se ejecuta dentro del mismo proceso

Ese es el flujo básico.

2.2 También puedes registrar funciones async

BackgroundTasks puede registrar no solo funciones def, sino también funciones async def.

import httpx
from fastapi import BackgroundTasks, FastAPI

app = FastAPI()

async def notify_webhook(payload: dict) -> None:
    async with httpx.AsyncClient(timeout=5.0) as client:
        await client.post("https://example.com/webhook", json=payload)

@app.post("/events")
async def create_event(background_tasks: BackgroundTasks):
    # Supongamos que se ha guardado algún evento
    background_tasks.add_task(notify_webhook, {"type": "created"})
    return {"status": "accepted"}

Incluso registrando funciones async, el momento de ejecución sigue siendo “después de enviar la respuesta”, pero FastAPI/Starlette se encarga de gestionarlas correctamente en el event loop.

2.3 Entender el mecanismo a grandes rasgos

Es útil pensar en BackgroundTasks simplemente como “una lista de funciones que se ejecutan después de la respuesta, dentro del mismo proceso de la aplicación”.

  • Las tareas no se ejecutan en otros procesos ni en otras máquinas
  • No hay cola ni mecanismo de reintentos
  • Si el proceso del worker muere, las tareas también se pierden

Así que es realmente adecuado para “tareas ligeras que pueden tardar unos pocos segundos y que quieres ejecutar tras la respuesta”.


3. Puntos de diseño y límites de BackgroundTasks

BackgroundTasks es cómodo, pero tiene fortalezas y debilidades. Si ordenas esto, te resultará mucho más sencillo juzgar cuándo necesitas pasar a una “cola de trabajos seria” como Celery.

3.1 Casos en los que encaja bien

  • Operaciones ligeras que tardan del orden de unos pocos a varios segundos
  • El volumen de tareas no es tan alto (decenas a cientos por minuto)
  • Está bien que el mismo número de instancias que el API REST soporte la carga
  • No es crítico si una tarea falla, o basta con que quede registrada en logs

Ejemplos concretos:

  • Enviar un único correo tras el registro de un usuario
  • Enviar logs ligeros o añadir registros de auditoría
  • Generar miniaturas de imágenes pequeñas

3.2 Casos en los que no encaja bien (ojo)

  • Necesitas procesar un número enorme de tareas (decenas de miles o más)
  • Las tareas duran varios minutos o decenas de minutos
  • Necesitas reintentos, ejecución diferida o programada
  • Quieres poder consultar el estado de las tareas (éxito/fallo/progreso) a posteriori

¿Por qué?

  • Si el proceso se cae, las tareas se pierden
  • Si crece el número de tareas en ejecución, tus workers de FastAPI se ahogan
  • Implementar tú mismo la gestión de estado y los reintentos supone mucho trabajo

Esa es la razón.

3.3 Errores típicos y contramedidas

  1. Meter tareas muy pesadas en CPU dentro de BackgroundTasks
    → Bloquearán el event loop y ralentizarán otras peticiones.
    → Las tareas ligadas a CPU deben derivarse a otro proceso (Celery, etc.) por seguridad.

  2. No manejar las excepciones
    → Si se produce una excepción dentro de una tarea, acaba solo en los logs y es difícil de ver desde el lado que llama.
    → Asegúrate de registrar correctamente las excepciones dentro de las tareas y enviar notificaciones si es necesario.

  3. Enviar un número masivo de tareas
    → Las respuestas se devolverán, pero el backend no podrá seguir el ritmo de la carga en segundo plano y poco a poco consumirá más memoria y CPU.
    → Decide un límite superior del tipo “este volumen es aceptable”, y cuando las cosas se vuelvan realmente pesadas, es el momento de pasar a Celery.


4. Fundamentos de la cola de tareas distribuida Celery

Cuando BackgroundTasks se queda corto, entra Celery en escena. Celery es una de las colas de tareas distribuidas más representativas en Python, usada en muchos sistemas en producción.

4.1 Componentes de Celery

Celery se compone, a grandes rasgos, de tres partes:

  1. Broker
    Gestiona las colas de tareas. Redis y RabbitMQ son las opciones más comunes.

  2. Worker
    Procesos que sacan tareas del broker y las ejecutan. Pueden distribuirse en varias máquinas.

  3. Result backend
    Almacena resultados y estados de las tareas. Se puede usar Redis o una base de datos.

FastAPI actúa como “el punto de entrada que encola las tareas”, mientras que el trabajo real lo realizan procesos de worker de Celery separados.

4.2 Estructura típica de directorios

myapp/
├─ app/
│  ├─ main.py        # FastAPI
│  ├─ celery_app.py  # Definición de la app de Celery
│  ├─ tasks.py       # Definición de tareas de Celery
│  └─ ...
├─ docker-compose.yml
└─ requirements.txt

Con esta estructura, a menudo ejecutarás FastAPI y los workers de Celery como contenedores separados.


5. Construyendo la configuración mínima de FastAPI + Celery

A partir de aquí, vamos a cablear FastAPI y Celery con un ejemplo sencillo.

5.1 Definir la app de Celery

# app/celery_app.py
from celery import Celery

celery_app = Celery(
    "myapp",
    broker="redis://redis:6379/0",      # Ejemplo suponiendo docker-compose
    backend="redis://redis:6379/1",
)

celery_app.conf.update(
    task_track_started=True,
    result_expires=3600,  # los resultados caducan tras 1 hora
)

Aquí usamos Redis tanto como broker como result backend.

5.2 Definir una tarea

# app/tasks.py
import time
from app.celery_app import celery_app

@celery_app.task(name="tasks.long_add")
def long_add(x: int, y: int) -> int:
    # Supongamos que este es un proceso que tarda
    time.sleep(10)
    return x + y

El decorador @celery_app.task registra una función como tarea, lo que permite llamarla mediante delay o apply_async.

5.3 Encolar tareas desde FastAPI

# app/main.py
from fastapi import FastAPI
from pydantic import BaseModel
from app.tasks import long_add

app = FastAPI(title="FastAPI + Celery sample")

class AddRequest(BaseModel):
    x: int
    y: int

@app.post("/jobs/add")
def enqueue_add(req: AddRequest):
    # Encolamos como tarea de Celery
    async_result = long_add.delay(req.x, req.y)
    return {"task_id": async_result.id}

@app.get("/jobs/{task_id}")
def get_result(task_id: str):
    result = long_add.AsyncResult(task_id)
    if not result.ready():
        return {"task_id": task_id, "status": result.status}
    if result.failed():
        return {"task_id": task_id, "status": "FAILURE"}
    return {
        "task_id": task_id,
        "status": "SUCCESS",
        "result": result.result,
    }

En este ejemplo:

  1. Hacer POST con {"x": 1, "y": 2} a /jobs/add encola la tarea y devuelve un ID de tarea.
  2. Hacer GET a /jobs/{task_id} para consultar el estado o el resultado.

5.4 Ejemplo de configuración con docker-compose

En proyectos reales, a menudo usarás docker-compose para levantar Redis y los workers de Celery junto con el API.

# docker-compose.yml (ejemplo)
version: "3.9"
services:
  api:
    build: .
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000
    depends_on:
      - redis
    ports:
      - "8000:8000"

  worker:
    build: .
    command: celery -A app.celery_app.celery_app worker --loglevel=INFO
    depends_on:
      - redis

  redis:
    image: redis:7
    ports:
      - "6379:6379"

Al ejecutar FastAPI y los workers de Celery como contenedores/procesos separados:

  • Desacoplas las respuestas del API del procesamiento pesado
  • Puedes escalar los workers horizontalmente con facilidad

Esos son los beneficios clave.


6. Comparando BackgroundTasks y Celery

Ahora que hemos visto BackgroundTasks de FastAPI y la cola de tareas distribuida Celery, ordenemos las diferencias en una tabla comparativa.

Aspecto BackgroundTasks Celery
Lugar de ejecución Mismo proceso que la app de FastAPI Procesos de worker separados; pueden estar en otros hosts
Dependencias Ninguna (solo FastAPI) Broker (Redis / RabbitMQ) y workers
Momento de ejecución Después de enviar la respuesta HTTP Una vez encolado, se ejecuta cuando haya un worker libre
Reintentos y ejecución diferida Hay que implementarlo a mano Soportado de serie (retry, countdown, etc.)
Gestión de estado de tareas La implementas tú IDs de tareas, estado y resultados gestionables
Escalado Unido al escalado de los workers del API Los workers se escalan de forma independiente
Mejor para Tareas ligeras y cortas Tareas pesadas, gran volumen, procesamiento tipo batch

Muy por encima:

  • Comienza con BackgroundTasks
  • Cuando la carga o los requisitos crezcan, pásate (o añade) Celery

Ese suele ser el enfoque práctico en proyectos reales.


7. Algunas funciones de Celery más prácticas

Ya que hemos llegado hasta aquí, veamos algunas funciones de Celery especialmente útiles en producción.

7.1 Reintentos

Las APIs externas o los servidores de correo a veces fallan temporalmente. Con Celery, puedes configurar reintentos fácilmente en la definición de la tarea.

# app/tasks.py
from celery import shared_task
from app.celery_app import celery_app
import httpx

@celery_app.task(bind=True, max_retries=3, default_retry_delay=10)
def send_webhook(self, url: str, payload: dict):
    try:
        resp = httpx.post(url, json=payload, timeout=5.0)
        resp.raise_for_status()
    except Exception as exc:
        # En caso de fallo, reintentar 10 segundos después (hasta 3 veces)
        raise self.retry(exc=exc)

7.2 Ejecución diferida y programada

Necesidades como “ejecutar dentro de 5 minutos” o “ejecutar todos los días a medianoche” también son fáciles de manejar.

# Ejecutar 5 minutos más tarde
send_webhook.apply_async(
    args=["https://example.com/webhook", {"foo": "bar"}],
    countdown=300,
)

Para programar ejecuciones periódicas, normalmente se combina Celery con celery beat u otros programadores.

7.3 Herramientas de monitorización y gestión (Flower, etc.)

Flower es una herramienta habitual para visualizar el estado de Celery vía interfaz web.

  • Listas de tareas en ejecución y pendientes
  • Historial de éxitos y fallos
  • Estado de los workers

Si usas Celery en producción, vale mucho la pena considerar herramientas como Flower.


8. Trucos para pruebas y desarrollo local

Cuanto más procesamiento en segundo plano tengas, más impacta en las pruebas y en el desarrollo local. Aquí van algunos trucos para que BackgroundTasks y Celery sean más amigables con los tests.

8.1 Probar BackgroundTasks

BackgroundTasks simplemente “registra una función para ser llamada”, así que en tests unitarios lo mejor es probar directamente la función de la tarea.

# app/tasks_local.py
def write_audit_log(user_id: int, action: str) -> None:
    ...

# app/main.py
from fastapi import BackgroundTasks

@app.post("/do-something")
async def do_something(background_tasks: BackgroundTasks):
    # Omitimos la lógica principal
    background_tasks.add_task(write_audit_log, 123, "do_something")
    return {"ok": True}

Para las pruebas:

  • Usa tests unitarios sobre write_audit_log para verificar su lógica
  • En tests de endpoints, limita a comprobar ligeramente que BackgroundTasks se llama correctamente

Ese enfoque en dos capas es realista.

8.2 Probar tareas de Celery (modo eager)

Celery tiene una opción task_always_eager. Si la activas, las tareas se ejecutan inmediatamente en el proceso local sin usar workers.

# tests/conftest.py o similar
from app.celery_app import celery_app

def pytest_configure():
    celery_app.conf.update(task_always_eager=True)

Con esto:

res = long_add.delay(1, 2)
assert res.result == 3

Puedes comprobar los resultados de forma síncrona durante las pruebas. (Solo recuerda poner task_always_eager=False otra vez para producción).


9. Puntos operativos a vigilar (más orientados a Celery)

Al ejecutar Celery en producción, ayuda mucho decidir de antemano los puntos siguientes para reducir problemas.

9.1 “Granularidad” de las tareas e idempotencia

  • Evita tareas que se ejecutan durante mucho tiempo (decenas de minutos a horas). Divídelas en tareas más pequeñas siempre que puedas.
  • Diseña las tareas para que sean idempotentes, es decir, seguras incluso si se ejecutan dos veces la misma tarea (cuidado con llamar dos veces a APIs externas).

Si mantienes la idempotencia en mente, es mucho más fácil razonar sobre “hasta dónde hemos llegado” al hacer reintentos o recuperación ante desastres.

9.2 Reintentos, timeouts y “dead letters”

  • Decide el número de reintentos y los intervalos (p. ej., máximo 3 veces, retroceso exponencial).
  • Establece un tiempo máximo de ejecución (timeout) para detener automáticamente procesos anormalmente largos.
  • Redirige las tareas que siguen fallando incluso después de reintentos a una cola de “dead letter” para su tratamiento especial.

Estos puntos se conectan con los SLO de tu sistema (cada cuánto deberían tener éxito las operaciones), así que es bueno alinearlos a nivel de equipo.

9.3 Logging y monitorización

  • Para tareas importantes, registra de forma clara inicio, éxito y fallo (se recomiendan logs en JSON).
  • Visualiza métricas clave como volumen de tareas por tipo, número de workers y tasas de fallo en dashboards.
  • Lanza alertas cuando la tasa de fallos suba de forma repentina.

Si diseñas logs y métricas junto con las de FastAPI, la resolución de problemas se vuelve mucho más sencilla cuando surge algún incidente.


10. Patrones de introducción según el tipo de lector

Basándonos en todo lo anterior, ordenemos “por dónde empezar” según tu situación.

10.1 Desarrolladores individuales y personas que están aprendiendo

  • Paso 1: Usa BackgroundTasks para mover el envío de correos y pequeñas tareas detrás de la respuesta.
  • Paso 2: Cuando aparezca alguna tarea de larga duración o alto volumen, prueba el ejemplo de Celery en local.
  • Paso 3: Usa docker-compose para ejecutar FastAPI + Celery + Redis juntos.

Llegado a este punto, debería estar mucho más claro “qué tienes que gestionar tú y hasta dónde”.

10.2 Ingenieros/as de backend en equipos pequeños

  • Paso 1: Revisa los APIs existentes en busca de partes que puedan descargarse a BackgroundTasks.
  • Paso 2: Para operaciones pesadas o de gran volumen, empieza dividiéndolas en workers de Celery.
  • Paso 3: Diseña tus colas de trabajo (nombres de colas y prioridades), añade logging de auditoría y visualiza todo con Flower u otras herramientas.

Un enfoque gradual de “primero usar Celery solo para algunas funcionalidades” es realista. No hace falta migrarlo todo de una vez.

10.3 Equipos de desarrollo de SaaS y startups

  • Separa colas por tipo de tarea (por ejemplo, emails, reports, integrations).
  • Ajusta el número de workers y recursos por cola para controlar prioridades.
  • Crea monitorización, alertas y dashboards para ver rápidamente “qué tarea está causando el cuello de botella”.

En esta fase, las responsabilidades se vuelven claras: “Mantener la app de FastAPI ligera y sencilla; mover todo lo pesado a Celery.”


11. Hoja de ruta de implantación (está bien ir paso a paso)

Por último, aquí tienes una hoja de ruta para introducir y mejorar el procesamiento en segundo plano a partir de ahora:

  1. Usa BackgroundTasks para descargar tareas ligeras como envío de correos y notificaciones webhook.
  2. Mide tiempos de procesamiento y volúmenes para identificar cuellos de botella.
  3. Extrae las tareas pesadas a tareas de Celery y prueba FastAPI + Celery en local con docker-compose.
  4. Proporciona un ID de tarea y un API de estado (como /jobs/{task_id}) para poder integrarlo con frontends y otros servicios.
  5. Configura reintentos, timeouts, idempotencia y monitorización, y ve derivando gradualmente el tráfico de producción hacia Celery.
  6. Según sea necesario, separa colas, escala workers e introduce herramientas de monitorización como Flower.

Hacer “todo de golpe” es realmente difícil, así que empezar con BackgroundTasks y dar pasos pequeños es muy recomendable.


Enlaces de referencia (documentación oficial y artículos)

Nota: El contenido es correcto en el momento de escribir esto. Consulta cada sitio para la información más reciente.


Resumen

  • Como regla general, deberías descargar al segundo plano las tareas por las que no quieres que las personas usuarias esperen, siempre que sea posible.
  • BackgroundTasks, incorporado en FastAPI, es muy cómodo y fácil de usar para ejecutar tareas ligeras tras la respuesta, pero no es adecuado para trabajos pesados o de gran volumen.
  • Para trabajo a gran escala, alta carga o con requisitos estrictos de reintentos y monitorización, combinar FastAPI con la cola de tareas distribuida Celery facilita mucho una operación estable.
  • Un enfoque por fases es práctico y seguro en proyectos reales: comienza con BackgroundTasks e introduce Celery cuando realmente lo necesites.

Este artículo ha quedado un poco largo, pero con suerte te ayudará a decidir “qué tareas descargar al segundo plano” y “a partir de dónde conviene usar Celery”.
Está perfectamente bien avanzar poco a poco, paso a paso: quizá puedas empezar probando el procesamiento en segundo plano con algo cercano, como el envío de correos o la generación de miniaturas.


por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)