Tareas en Segundo Plano (Background Jobs)
Cómo programar y ejecutar tareas en segundo plano usando cron, colas de trabajo y workers.
Visión General
Las tareas en segundo plano descargan trabajo lento o no crítico del ciclo de petición/respuesta. Enviar emails, generar reportes, procesar imágenes o sincronizar con APIs de terceros nunca deberían bloquear una petición HTTP del usuario. A continuacion se implementa colas de tareas, programación con cron y patrones de workers en Python, JavaScript y Java.
Cuándo Usar
Usa este recurso cuando:
- Envíes emails o SMS que pueden esperar unos segundos. Consulta Email Templates MJML para generación de contenido de email.
- Generes exportaciones, reportes o PDFs que toman >1s. Consulta Generate PDFs para generación de documentos.
- Proceses imágenes, videos o documentos subidos por usuarios. Consulta Image Optimization para procesamiento de media.
- Sincronices datos con APIs externas según un horario. Consulta Call REST API para patrones de clientes API.
- Agregues análisis o ejecutes tareas de limpieza nocturnas. Consulta Scheduled Jobs para tareas cron serverless.
Solución
Python (Celery + Redis)
from celery import Celery
from celery.schedules import crontab
app = Celery("tasks", broker="redis://localhost:6379/0", backend="redis://localhost:6379/0")
@app.task(bind=True, max_retries=3)
def send_email(self, to, subject, body):
try:
print(f"Sending email to {to}")
except Exception as exc:
raise self.retry(exc=exc, countdown=60)
@app.task
def generate_report(user_id):
print(f"Generating report for user {user_id}")
return f"/reports/{user_id}.pdf"
# Programar tareas periódicas
app.conf.beat_schedule = {
"daily-cleanup": {
"task": "tasks.cleanup_old_logs",
"schedule": crontab(hour=2, minute=0),
},
}
# Encolar desde la app web
send_email.delay("alice@example.com", "Welcome", "Hello!")
JavaScript (BullMQ + Redis)
const { Queue, Worker } = require("bullmq");
const IORedis = require("ioredis");
const connection = new IORedis({ host: "localhost", port: 6379, maxRetriesPerRequest: null });
const emailQueue = new Queue("emails", { connection });
const worker = new Worker("emails", async (job) => {
const { to, subject, body } = job.data;
console.log(`Sending email to ${to}`);
return { sent: true };
}, { connection });
// Agregar trabajo desde una ruta de API
async function enqueueEmail(to, subject, body) {
await emailQueue.add("send-email", { to, subject, body }, {
attempts: 3,
backoff: { type: "exponential", delay: 1000 },
});
}
// Cron job con BullMQ
const cronQueue = new Queue("cron", { connection });
await cronQueue.add("cleanup", {}, { repeat: { cron: "0 2 * * *" } });
Java (ScheduledExecutorService + Spring @Scheduled)
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class JobService {
// Cron job: corre todos los días a las 2 AM
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupOldLogs() {
System.out.println("Running nightly cleanup");
}
// Fixed rate: corre cada 5 minutos
@Scheduled(fixedRate = 300_000)
public void syncExternalData() {
System.out.println("Syncing with external API");
}
}
// Ejecución async manual
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.initialize();
CompletableFuture.runAsync(() -> {
System.out.println("Running background task");
}, executor);
Explicación
Las tareas en segundo plano separan qué debe suceder de cuándo sucede. La arquitectura básica tiene tres componentes:
- Productor (API): Encola un trabajo cuando ocurre un evento (usuario se registra, archivo subido).
- Broker (Cola): Redis, RabbitMQ o Amazon SQS retienen trabajos de forma duradera hasta que un worker los toma.
- Consumidor (Worker): Un proceso separado sondea la cola y ejecuta trabajos. Los workers pueden correr en máquinas diferentes a la API web.
Los cron jobs son un caso especial: en vez de activarse por eventos de usuario, corren según un horario. La mayoría de sistemas de cola (Celery Beat, BullMQ, Spring @Scheduled) soportan ambos patrones.
Variantes
| Herramienta | Lenguaje | Persistencia | Programación | Ideal Para |
|---|---|---|---|---|
| Celery + Redis | Python | Redis (o RabbitMQ) | Celery Beat | Colas de tareas completas |
| BullMQ | JavaScript | Redis | Cron integrado | Proyectos Node.js, TypeScript |
| Sidekiq | Ruby | Redis | Sidekiq-cron | Ruby on Rails |
| Hangfire | C# | SQL Server/Redis | Integrado | Ecosistema .NET |
| Spring @Scheduled | Java | N/A (en proceso) | Expresiones cron | Tareas programadas simples |
| AWS Lambda + EventBridge | Cualquiera | N/A (serverless) | Reglas EventBridge | Cloud-native, pago por uso |
Lo que funciona
- Haz trabajos idempotentes: Ejecutar el mismo trabajo dos veces debería producir el mismo resultado. Usa IDs únicos de trabajo para prevenir duplicados.
- Configura reintentos con backoff: Fallos transitorios (cortes de red) deberían reintentar 3-5 veces con backoff exponencial.
- Loguea contexto del trabajo: Incluye job ID, user ID y timestamp en cada línea de log para debugging.
- Separa colas por prioridad: Pon procesamiento de pagos en una cola
high, envío de emails endefault. - Monitorea dead letter queues: Trabajos que fallan todos los reintentos necesitan inspección manual. Alerta cuando la DLQ crece.
Errores Comunes
- Ejecutar tareas pesadas en el proceso web: Generar un PDF de 100 páginas durante una petición HTTP provocará timeout y degradará la experiencia del usuario.
- Sin reintentos ni manejo de dead letter: Un reinicio de Redis puede perder todos los trabajos pendientes si no los persistes.
- Asumir timing exacto de cron: Cron es “correr en o después” del horario programado, no exactamente en ese momento. No dependas de precisión de milisegundos.
- No manejar caídas de workers: Si un worker muere en medio de un trabajo, puede perderse. Usa acknowledgments y visibility timeouts.
- Sobrecargar la cola: Encolar 100K trabajos a la vez puede abrumar a los workers. Usa rate limiting o encolado por lotes.
Tips de Rendimiento
- Batchea jobs pequeños. Procesar 1000 items uno por uno es más lento que batchear:
@app.task
def process_batch(item_ids):
items = Item.objects.filter(id__in=item_ids)
for item in items.iterator():
process(item)
# Encolar en lotes de 100
for i in range(0, len(ids), 100):
process_batch.delay(ids[i:i+100])
- Usa prefetch limits. Evita que un worker acapare todos los jobs:
# Cada worker fetchea solo 1 job a la vez
celery -A tasks worker --prefetch-multiplier=1 --concurrency=4
- Separa colas por requisitos de latencia. Jobs real-time necesitan workers dedicados:
# Worker dedicado para cola real-time (sin tareas long-running bloqueando)
celery -A tasks worker -Q realtime --concurrency=8
# Worker general para cola batch
celery -A tasks worker -Q batch --concurrency=2 Preguntas frecuentes
Debo usar Redis o RabbitMQ para mi cola de tareas?
Redis es más simple de operar y suficiente para la mayoría de cargas (<10K trabajos/seg). RabbitMQ ofrece mejores garantías de durabilidad, flexibilidad de routing y soporte del protocolo AMQP. Para datos financieros o de salud críticos, RabbitMQ o Amazon SQS es más seguro. Para la mayoría de apps web, Redis está bien.
Cómo paso payloads grandes a un trabajo en segundo plano?
No pases datos grandes en el trabajo mismo. Guarda los datos en una base de datos o almacenamiento de objetos (S3) y pasa solo el ID al worker. Esto mantiene la cola ligera y evita que Redis/RabbitMQ se quede sin memoria.
Qué pasa si un worker se cae mientras procesa un trabajo?
Depende del sistema de cola. Celery usa acknowledgments: el trabajo se elimina de la cola solo después de completarse. BullMQ usa un visibility timeout: si el worker no completa el trabajo a tiempo, reaparece en la cola. Spring @Scheduled corre en-proceso, así que una caída de JVM pierde la tarea en vuelo. Diseña siempre para entrega al-menos-una-vez y trabajos idempotentes.
Docker Compose para Desarrollo Local
# docker-compose.yml
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
worker:
build: .
command: celery -A tasks worker --loglevel=info --concurrency=4
depends_on: [redis]
environment:
- REDIS_URL=redis://redis:6379/0
beat:
build: .
command: celery -A tasks beat --loglevel=info
depends_on: [redis, worker]
environment:
- REDIS_URL=redis://redis:6379/0
api:
build: .
ports: ["8000:8000"]
depends_on: [redis]
environment:
- REDIS_URL=redis://redis:6379/0
Colas con Prioridades en Celery
from celery import Celery
app = Celery("tasks", broker="redis://localhost:6379/0")
# Definir colas con prioridades
app.conf.task_queues = {
"high": Queue("high", routing_key="high.#"),
"default": Queue("default", routing_key="default.#"),
"low": Queue("low", routing_key="low.#"),
}
app.conf.task_routes = {
"tasks.process_payment": {"queue": "high"},
"tasks.send_email": {"queue": "default"},
"tasks.cleanup_logs": {"queue": "low"},
}
# Encolar con prioridad
process_payment.apply_async(args=[order_id], queue="high")
Manejo de Dead Letter Queue en BullMQ
const { Queue, Worker, QueueEvents } = require("bullmq");
const emailQueue = new Queue("emails", { connection });
// Worker con manejo de jobs fallidos
const worker = new Worker("emails", async (job) => {
const { to, subject, body } = job.data;
if (to.includes("fail")) throw new Error("Simulated failure");
console.log(`Email sent to ${to}`);
}, {
connection,
attempts: 3,
backoff: { type: "exponential", delay: 2000 },
});
// Escuchar jobs fallidos
worker.on("failed", (job, err) => {
console.error(`Job ${job.id} failed: ${err.message}`);
// Mover a dead letter queue para inspección manual
dlqQueue.add("failed-email", job.data, { removeOnComplete: true });
});
// Alertar cuando la DLQ crece
const dlqEvents = new QueueEvents("dlq", { connection });
dlqEvents.on("completed", () => {
console.warn("DLQ tiene nuevas entradas — inspección manual necesaria");
});
Tracking de Progreso de Jobs
from celery import Celery
app = Celery("tasks", broker="redis://localhost:6379/0")
@app.task(bind=True)
def long_running_task(self, items):
total = len(items)
for i, item in enumerate(items):
process(item)
self.update_state(
state="PROGRESS",
meta={"current": i + 1, "total": total, "percent": (i + 1) / total * 100}
)
return {"status": "complete", "processed": total}
# Verificar progreso desde la API
from celery.result import AsyncResult
result = AsyncResult(task_id)
print(result.info) # {'current': 50, 'total': 100, 'percent': 50.0}
Recursos Relacionados
Tareas programadas con Cron
Cómo programar y gestionar tareas recurrentes usando sintaxis cron en Linux, Python y Node.js.
RecipeVariables de Entorno
Cómo leer, establecer y gestionar variables de entorno de forma segura en Python, JavaScript y Java.
RecipeEndpoint de Health Check
Cómo implementar un endpoint de health check listo para producción para monitoreo y load balancers.
PatternPatrón Command
Encapsula una petición como un objeto, permitiendo parametrizar clientes con colas, logs y operaciones deshacibles. Patrón de diseño conductual.
PatternPatrón Abstract Factory
Crea familias de objetos relacionados sin especificar sus clases concretas. Patrón de diseño creacional para familias de objetos consistentes.
RecipeConfigurar CI con GitLab Pipelines
Cómo configurar pipelines de GitLab CI/CD para testing, building y deployment usando .gitlab-ci.yml con stages, jobs, caching y runners.