Tareas en segundo plano con el framework de tasks

Avanzado
Django
Django
Actualizado: 26/04/2026

El problema de las tareas largas en la petición HTTP

Cuando una vista Django tiene que enviar un correo, generar un PDF, sincronizar datos con un proveedor o recalcular métricas, el usuario queda bloqueado esperando a que termine el trabajo. Esto degrada la experiencia, consume hilos del servidor web y complica los timeouts del balanceador de carga. Una regla sencilla es mover todo lo que no afecte al resultado inmediato a una tarea en segundo plano.

Django 5.2 introdujo un framework de tasks oficial (django.tasks) que resuelve este patrón con una API uniforme: se declara la tarea, se encola desde cualquier parte del código y se procesa con un worker externo que lee la cola.

flowchart LR
    A[Vista HTTP] --> B[task.enqueue]
    B --> C["(Cola de trabajos)"]
    D[Worker externo] --> C
    D --> E[Ejecución de la tarea]
    E --> F[Resultado o efecto]
    A -.-> G[Respuesta HTTP inmediata]

El diagrama refleja la separación entre el proceso que crea el trabajo (la vista HTTP) y el proceso que lo ejecuta (un worker independiente). Esa frontera permite escalar el procesamiento sin mezclarlo con el servidor web y garantiza que las tareas sobrevivan a un reinicio de los contenedores web.

Definir y encolar tareas

Una tarea se escribe como una función de Python decorada con @task. La función puede vivir en cualquier módulo importable del proyecto, aunque la convención recomendada es agruparlas en un módulo tasks.py dentro de cada app.

# catalogo/tasks.py
from django.core.mail import send_mail
from django.tasks import task
from .models import Pedido

@task
def enviar_confirmacion_pedido(pedido_id: int) -> None:
    pedido = Pedido.objects.select_related("cliente").get(pk=pedido_id)
    send_mail(
        subject=f"Pedido {pedido.referencia} confirmado",
        message=f"Hola {pedido.cliente.nombre}, tu pedido ha sido registrado.",
        from_email=None,
        recipient_list=[pedido.cliente.email],
    )

La vista encola la tarea con enqueue, una operación muy rápida que solo escribe el trabajo en el backend y devuelve un identificador:

# catalogo/views.py
from django.http import JsonResponse
from .models import Pedido
from .tasks import enviar_confirmacion_pedido

def crear_pedido(request):
    pedido = Pedido.objects.create(...)
    enviar_confirmacion_pedido.enqueue(pedido_id=pedido.id)
    return JsonResponse({"pedido_id": pedido.id}, status=201)

La configuración del framework vive en settings.py bajo la variable TASKS. Cada backend registrado se asocia a un alias y permite rutar distintas tareas a colas diferentes.

# settings.py
TASKS = {
    "default": {
        "BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
    },
    "emails": {
        "BACKEND": "django.tasks.backends.database.DatabaseBackend",
    },
}

En desarrollo suele bastar con ImmediateBackend, que ejecuta la tarea en el mismo proceso y facilita depurar. En producción se opta por DatabaseBackend o por proveedores externos como Redis Queue, Celery o un provider gestionado, dependiendo de la infraestructura del equipo.

Workers, idempotencia y observabilidad

El framework separa la creación de la tarea (que ocurre en el proceso web) de la ejecución (que corre en un worker dedicado). Esa separación aporta resiliencia y permite escalar cada rol de forma independiente: más réplicas web en horas de tráfico y más workers cuando se acumulan trabajos pesados.

El worker se arranca con un comando de management y se despliega como proceso separado, típicamente en un Deployment aparte de Kubernetes o en un servicio gestionado.

python manage.py tasks_worker --queue default --concurrency 4

Tres aspectos son decisivos en el diseño de tareas profesionales:

Idempotencia: si el worker se reinicia y una tarea se reintenta, el resultado debe ser el mismo que si se hubiera ejecutado una sola vez. Patrones útiles son claves de idempotencia, verificación del estado actual antes de aplicar cambios y transacciones atómicas con select_for_update.

Observabilidad: cada tarea debería registrar su inicio, fin, duración y errores con contexto suficiente para depurar incidentes. Los logs estructurados con pedido_id, cliente_id y correlation_id permiten rastrear una ejecución completa desde la petición HTTP hasta la finalización del worker.

Frontera de responsabilidad: la vista decide qué trabajo se encola y con qué argumentos, pero la tarea contiene la lógica pesada, las validaciones operativas y el manejo de errores. Mezclar ambas responsabilidades lleva a código difícil de testear y a duplicar reglas de negocio.

import logging
from django.tasks import task

logger = logging.getLogger(__name__)

@task(retry=3, backoff=5)
def sincronizar_inventario(sku: str) -> None:
    logger.info("Sincronizando %s", sku)
    # Idempotente: solo actualiza si hay cambios reales
    producto = Producto.objects.select_for_update().get(sku=sku)
    cambios = proveedor_externo.consultar(sku)
    if cambios:
        producto.stock = cambios.stock
        producto.save(update_fields=["stock"])
    logger.info("Sync completo %s", sku)

El decorador @task admite parámetros como retry, backoff y timeout para definir la política de reintentos. La combinación de reintentos automáticos e idempotencia reduce drásticamente las incidencias operativas.

Caso B2B: notificaciones masivas en una clínica online

Una clínica online con 200.000 pacientes activos envía recordatorios de cita el día anterior. En lugar de bloquear el cron job mientras manda los correos, una tarea Django encola 200.000 trabajos individuales en la cola emails, cada uno con el paciente_id como clave de idempotencia. Tres workers consumen la cola con concurrencia 10 y mantienen la latencia bajo control. Si el servicio SMTP falla transitoriamente, el decorador reintenta con backoff exponencial y registra el incidente en el sistema de logs centralizado; los pacientes reciben su recordatorio aunque haya incidencias puntuales en el proveedor de correo.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Django

Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Declarar tareas con @task, encolarlas desde vistas o servicios, configurar backends en desarrollo y producción y diseñar tareas idempotentes con observabilidad adecuada.