Volver al Blog
SoftwareDesarrolloSaaS

Colas de Procesamiento en Segundo Plano en Next.js con BullMQ y Redis: Arquitectura para Tareas Pesadas

Brayan Developer
6 min de lectura
Colas de Procesamiento en Segundo Plano en Next.js con BullMQ y Redis: Arquitectura para Tareas Pesadas
Aprende a desacoplar tareas pesadas, generación de reportes y envíos masivos en Next.js utilizando colas de trabajo distribuidas con BullMQ, Redis y Workers dedicados.

En aplicaciones web modernas construidas sobre Next.js (App Router y Server Actions), ejecutar operaciones lentas —como generación de PDFs, compresión de video, sincronización de catálogos o despachos de correo masivo— directamente en el ciclo de solicitud HTTP bloquea la respuesta al usuario y agota los tiempos de espera (timeouts) en plataformas de despliegue como Vercel o VPS. La solución definitiva consiste en implementar una arquitectura asíncrona desacoplada mediante colas de trabajo (Message Queues) con BullMQ y Redis. En esta guía técnica exploramos su diseño, concurrencia, reintentos exponenciales y monitoreo en producción.

Portada

El Problema del Bloqueo HTTP en Server Actions y APIs#

Cuando un usuario solicita la exportación de un reporte financiero con miles de transacciones o el procesamiento de una orden de compra, esperar sincrónicamente a que el servidor complete la tarea degrada la experiencia de usuario (INP - Interaction to Next Paint) y genera cuellos de botella en la base de datos:

[Cliente Web / App]
       │
       │ POST /api/generar-reporte (HTTP Request)
       ▼
[Next.js Server Action] ──► [Generación de PDF pesada (12 segundos)] ──► [Bloqueo de conexión]
       ▲                                                                       │
       └────────────────────────────────── Timeout 504 / Fallo de red ────────┘

Con una cola asíncrona distribuida, la petición HTTP responde en milisegundos (202 Accepted), encolando un mensaje que será procesado por workers en segundo plano:

[Cliente Web / App]
       │
       │ POST /api/generar-reporte
       ▼
[Next.js Server Action] ──► [Redis Queue (BullMQ)] ──► Respuesta 200 OK (en 15ms)
                                   │
                                   ▼ (Despacho asíncrono)
                         [Worker Pool en Node.js]
                         ├── Worker #1: Exportación PDF
                         ├── Worker #2: Enlace a S3 / Supabase Storage
                         └── Worker #3: Notificación Push / Webhook

Configuración del Cliente BullMQ y la Conexión Redis#

Para garantizar alto rendimiento sin fugas de memoria (connection leaks), configuramos la conexión compartida con ioredis:

// lib/queue/connection.ts
import { RedisOptions } from "ioredis";

export const redisConnection: RedisOptions = {
  host: process.env.REDIS_HOST || "127.0.0.1",
  port: Number(process.env.REDIS_PORT) || 6379,
  password: process.env.REDIS_PASSWORD || undefined,
  maxRetriesPerRequest: null, // Requisito crítico de BullMQ para bloqueo de conexiones
  enableReadyCheck: false,
};

A continuación, creamos la definición de la cola tipada para Next.js:

// lib/queue/reportQueue.ts
import { Queue } from "bullmq";
import { redisConnection } from "./connection";

export interface ReportJobData {
  userId: string;
  reportType: "sales" | "inventory" | "tax_audit";
  dateRange: {
    start: string;
    end: string;
  };
  destinationEmail: string;
}

export const REPORT_QUEUE_NAME = "report-generation-queue";

// Instancia singleton para encolar desde Server Actions y API Routes
export const reportQueue = new Queue<ReportJobData>(REPORT_QUEUE_NAME, {
  connection: redisConnection,
  defaultJobOptions: {
    attempts: 3, // Reintentos automáticos
    backoff: {
      type: "exponential",
      delay: 2000, // 2s, 4s, 8s
    },
    removeOnComplete: {
      age: 3600, // Mantener historial 1 hora
      count: 1000,
    },
    removeOnFail: {
      age: 86400, // Mantener errores 24 horas para auditoría
    },
  },
});

Encolado Asíncrono desde Server Actions en Next.js 15#

Desde cualquier componente de React con Server Actions podemos despachar el trabajo de forma inmediata y no bloqueante:

// app/actions/reports.ts
"use server";

import { reportQueue, ReportJobData } from "@/lib/queue/reportQueue";
import { revalidatePath } from "next/cache";

export async function requestReportAction(formData: FormData) {
  const userId = formData.get("userId") as string;
  const reportType = formData.get("reportType") as ReportJobData["reportType"];
  const email = formData.get("email") as string;

  if (!userId || !reportType || !email) {
    return { success: false, error: "Datos incompletos para procesar el reporte." };
  }

  // Encolar trabajo en Redis
  const job = await reportQueue.add(
    `report-${reportType}-${userId}`,
    {
      userId,
      reportType,
      dateRange: {
        start: formData.get("startDate") as string,
        end: formData.get("endDate") as string,
      },
      destinationEmail: email,
    },
    {
      priority: reportType === "tax_audit" ? 1 : 3, // Prioridad alta para auditorías
    }
  );

  revalidatePath("/dashboard/reports");

  return {
    success: true,
    jobId: job.id,
    message: "Tu reporte está en cola. Recibirás una notificación cuando esté listo.",
  };
}

Implementación del Worker Independiente con Control de Concurrencia#

El procesamiento real del trabajo no debe ejecutarse en el proceso web de Next.js, sino en un proceso Node.js dedicado o contenedor Docker (worker.ts):

// workers/reportWorker.ts
import { Worker, Job } from "bullmq";
import { redisConnection } from "../lib/queue/connection";
import { REPORT_QUEUE_NAME, ReportJobData } from "../lib/queue/reportQueue";

export const reportWorker = new Worker<ReportJobData>(
  REPORT_QUEUE_NAME,
  async (job: Job<ReportJobData>) => {
    console.log(`[Worker] Iniciando procesamiento de Job ID: ${job.id} para usuario ${job.data.userId}`);

    // 1. Reportar progreso reactivo
    await job.updateProgress(10);

    // 2. Simulación de extracción de base de datos pesada
    await new Promise((resolve) => setTimeout(resolve, 3000));
    await job.updateProgress(50);

    // 3. Generación de PDF / Excel
    await new Promise((resolve) => setTimeout(resolve, 2000));
    await job.updateProgress(90);

    // 4. Enviar notificación por email / webhook
    console.log(`[Worker] Enlace de descarga enviado a: ${job.data.destinationEmail}`);
    await job.updateProgress(100);

    return {
      status: "COMPLETED",
      fileUrl: `https://storage.empresa.com/reports/${job.id}.pdf`,
      processedAt: new Date().toISOString(),
    };
  },
  {
    connection: redisConnection,
    concurrency: 5, // Procesa hasta 5 reportes en paralelo por worker
    limiter: {
      max: 20, // Máximo 20 trabajos por segundo para proteger APIs de terceros
      duration: 1000,
    },
  }
);

reportWorker.on("completed", (job) => {
  console.log(`✅ Job ${job.id} completado con éxito.`);
});

reportWorker.on("failed", (job, err) => {
  console.error(`❌ Job ${job?.id} falló tras ${job?.attemptsMade} intentos. Causa: ${err.message}`);
});

Monitoreo en Tiempo Real con Bull-Board#

Para tener visibilidad total de las tareas activas, pausadas, fallidas y completadas, integramos la UI administrativa de @bull-board:

// app/api/admin/queues/route.ts
import { createBullBoard } from "@bull-board/api";
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
import { HonoAdapter } from "@bull-board/hono";
import { reportQueue } from "@/lib/queue/reportQueue";

// Integración con panel de monitoreo protegido por sesión y roles
const serverAdapter = new HonoAdapter();
serverAdapter.setBasePath("/api/admin/queues");

createBullBoard({
  queues: [new BullMQAdapter(reportQueue)],
  serverAdapter: serverAdapter,
});

Mejores Prácticas en Producción#

  1. Idempotencia en los Trabajos: Si un worker se reinicia a mitad de la ejecución, el reintento no debe duplicar cobros ni registros. Utiliza jobId determinísticos.
  2. Aislamiento de Recursos: Ejecuta el proceso de Next.js y los Workers de BullMQ en contenedores Docker separados para que un pico de carga de procesamiento no afecte el tiempo de respuesta web.
  3. Dead Letter Queue (DLQ): Configura colas de descarte para inspeccionar y reintentar manualmente trabajos que agotaron sus intentos máximos.

Conclusión#

Implementar colas de segundo plano con BullMQ y Redis transforma una aplicación Next.js en un sistema empresarial robusto capaz de manejar cargas masivas, procesamiento asíncrono fiable y tiempos de respuesta ultrarrápidos.

¿Necesitas diseñar arquitecturas de alta disponibilidad, sistemas SaaS escalables o procesamiento en segundo plano para tu empresa? Descubre nuestros servicios de Desarrollo de Software a Medida o contáctanos para una asesoría técnica.

Etiquetas
BullMQRedisNext.jsBackground JobsSaaSNode.js
Compartir:XLinkedInWhatsApp

¿Te gustaría profundizar en estos temas?

Aprende sobre desarrollo de software, apps a medida, automatizaciones con N8N, Next.js y Cloud con casos reales.

Hablemos