Vercel Daemons: workers de colas junto a tu app Next.js
Vercel Daemons (beta) ejecuta workers de BullMQ, Celery o Temporal en el mismo proyecto que tu app Next.js. Cómo se configura, cuánto cuesta y cuándo no usarlo.

Tu endpoint devuelve un 202 Accepted. El job entra en la cola. En local se procesa en dos segundos. En producción no se procesa nunca.
No es un bug tuyo. Es el modelo de Vercel Functions, y es justo lo que viene a resolver Vercel Daemons: la función responde, la ejecución termina y el trabajo asíncrono que quedara colgando muere con ella.
Por eso la arquitectura "seria" con Next.js en Vercel siempre ha tenido una pieza fuera: el worker de BullMQ o de Celery en Railway, Render, Fly o un VPS. Dos plataformas, dos facturas, dos sitios donde mirar logs.
El 15 de octubre de 2026 Vercel anunció Vercel Daemons en beta pública. Y esa pieza puede volver al mismo proyecto.
En corto: Vercel Daemons ejecuta contenedores que se quedan encendidos y sacan trabajo de una cola, una base de datos o un servicio externo, sin servidor HTTP. Se declaran con una entrada daemons en vercel.json, vercel.ts o vercel.toml, y Vercel mantiene las instancias que fijes por región. Compensa si tu worker es pequeño y estable; no, si necesitas autoescalado, previews o disco persistente.
¿Qué es Vercel Daemons?
Vercel Daemons es un recurso de Vercel que ejecuta procesos de larga duración, empaquetados como contenedor desde un Dockerfile o Containerfile, que piden su propio trabajo en lugar de recibir peticiones. Corren sobre Fluid compute, la misma plataforma que las Vercel Functions, y se despliegan con tu app, en el mismo proyecto.
La documentación oficial y el changelog del lanzamiento citan tres usos: consumidores de colas (Vercel Queues en modo poll, SQS, Kafka, Redis con BullMQ o Celery), workers de Temporal o Cadence que hacen long-poll de su cola de tareas, y runners de CI autoalojados (GitHub Actions, GitLab, CircleCI) que esperan jobs sin conexiones entrantes.
La diferencia con una función es de dirección. Una función espera a que le llegue una petición. Un daemon sale a buscar el trabajo. Vercel lo resume en su guía de elección: las imágenes de contenedor cambian qué ejecuta una función; los Daemons cambian cómo se ejecuta un worker.
Por qué un worker de BullMQ no funcionaba en Vercel
No funcionaba porque BullMQ necesita un proceso vivo escuchando Redis, y una función de Vercel deja de existir cuando entrega la respuesta.
No es teoría. En este hilo de la comunidad de Vercel (agosto de 2024), un developer describe el caso de la apertura: Next.js encola jobs de OpenAI con BullMQ, recibe un 202 y en producción nada se procesa.
La respuesta del equipo de Vercel: las funciones detienen la ejecución cuando la respuesta se completa y el trabajo asíncrono pendiente se termina. Las alternativas que propusieron (waitUntil, streaming, más duración máxima) alargan la vida de la función, pero no la convierten en un proceso.
waitUntil() y after() comparten el límite de duración de la función. Un worker de cola necesita un bucle infinito. Son modelos distintos.
Ojo: tener el proceso vivo no arregla un worker que bloquea el event loop de Node. Con Daemons ese problema solo cambia de máquina.
Cómo configurar Vercel Daemons: el ejemplo oficial con Celery
Se configura con un Dockerfile en la carpeta del worker y una entrada daemons en el archivo de configuración de la raíz: vercel.json, vercel.ts o vercel.toml.
Este es el ejemplo del changelog oficial: un worker de Celery, una instancia, en iad1 (Washington, D.C.), con el Dockerfile en worker/.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"daemons": {
"celery": {
"root": "worker/",
"entrypoint": "Dockerfile",
"command": ["celery", "-A", "app", "worker", "--loglevel=info"],
"instances": { "iad1": 1 }
}
}
}
Según la referencia de configuración:
root,entrypointeinstancesson obligatorios.instancesva de0a100por región.commandsustituye elENTRYPOINTy elCMDde la imagen, sin pasar por una shell.resources.requests.memoryadmite"1Gi","2Gi"(por defecto),"4Gi"u"8Gi", con 1, 1, 2 o 4 vCPU.terminationGracePeriodSecondsfija la espera tras elSIGTERM: 30 segundos por defecto, hasta 3.600.
Un detalle que te ahorra un build fallido: el nombre del daemon solo admite minúsculas separadas por - o _. worker1 no vale.
Un worker de BullMQ en TypeScript con Vercel Daemons
En un proyecto Next.js con BullMQ, el worker vive en su propia carpeta con su Dockerfile, y Next.js solo encola.
app/ # tu Next.js de siempre
workers/emails/
.dockerignore
Dockerfile
package.json
tsconfig.json
src/schema.ts # el contrato del job, solo importa zod
src/worker.ts
vercel.json
El schema del job va en su propio archivo, sin efectos al cargarse:
// workers/emails/src/schema.ts
import { z } from 39;zod39;;
export const welcomeEmailSchema = z.object({
userId: z.string(),
to: z.email(),
});
export type WelcomeEmail = z.infer<typeof welcomeEmailSchema>;
El worker cuida dos cosas: validar el payload antes de tocarlo y cerrar bien al recibir SIGTERM.
// workers/emails/src/worker.ts
import { Worker, type Job } from 39;bullmq39;;
import { Redis } from 39;ioredis39;;
import { welcomeEmailSchema, type WelcomeEmail } from 39;./schema.js39;;
// Sin REDIS_URL, ioredis conectaría a localhost en silencio y la instancia
// se reiniciaría en bucle sin decir por qué
const redisUrl = process.env.REDIS_URL;
if (!redisUrl) throw new Error(39;REDIS_URL no está definida39;);
// BullMQ exige maxRetriesPerRequest: null en las conexiones de los workers
const connection = new Redis(redisUrl, {
maxRetriesPerRequest: null,
});
async function sendWelcomeEmail(payload: WelcomeEmail) {
// Aquí va tu proveedor de email. Debe ser idempotente:
// el mismo job puede ejecutarse dos veces si la instancia se reinicia.
}
const worker = new Worker(
39;emails39;,
async (job: Job) => {
// Un payload mal formado falla aquí, no a mitad del envío
const payload = welcomeEmailSchema.parse(job.data);
await sendWelcomeEmail(payload);
},
{ connection, concurrency: 5 },
);
worker.on(39;failed39;, (job, error) => {
// stdout y stderr llegan a los Logs del proyecto en Vercel
console.error(`Job ${job?.id} falló:`, error.message);
});
process.on(39;SIGTERM39;, async () => {
// Deja de coger jobs nuevos y espera a que terminen los activos
await worker.close();
await connection.quit();
process.exit(0);
});
Validar job.data no es un capricho: un job encolado hace tres deploys puede traer un formato que tu worker ya no espera. El schema vive en el worker y el Route Handler importa workers/emails/src/schema.ts (nunca worker.ts, que abriría una conexión a Redis al cargarse), porque el contexto de build del daemon no ve el resto del repo. La app de Next.js necesita también Zod 4 en su propio package.json (npm i zod@^4). En el curso de Zod trabajamos la validación de datos que llegan de fuera.
El package.json del worker, con Zod 4 (z.email() no existe en Zod 3) e ioredis explícito:
{
"name": "emails-worker",
"private": true,
"type": "module",
"scripts": { "build": "tsc" },
"dependencies": { "bullmq": "^6.3.0", "ioredis": "^6.0.0", "zod": "^4.0.0" },
"devDependencies": { "typescript": "^5.9.0", "@types/node": "^24.0.0" }
}
Y su tsconfig.json, para que npm run build deje el dist/worker.js que arranca la imagen:
{
"compilerOptions": {
"target": "es2023",
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true
}
}
Genera el package-lock.json con npm install dentro de workers/emails/: sin él, npm ci falla.
El Dockerfile, basado en el quick start oficial (que usa node:24-slim), con un paso de build para compilar el TypeScript:
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
CMD ["node", "dist/worker.js"]
Añade un .dockerignore con node_modules y dist. Sin él, COPY . . mete en la imagen el node_modules de tu máquina, y en Windows o Mac puede traer binarios que no sirven en linux/amd64.
La misma imagen que corres en local es la que despliegas, la idea de los entornos reproducibles con Docker. Y la entrada en vercel.json, con más grace period para los emails en curso:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"daemons": {
"emails": {
"root": "workers/emails/",
"entrypoint": "Dockerfile",
"instances": { "iad1": 1 },
"resources": { "requests": { "memory": "1Gi" } },
"terminationGracePeriodSeconds": 60
}
}
}
En Next.js solo cambia una línea: añade "workers" al exclude del tsconfig.json de la raíz, o next build intentará comprobar los tipos del worker con las dependencias de la app.
Después, un Route Handler hace queue.add('welcome', { userId, to }) contra el mismo REDIS_URL, que llega a las dos piezas porque cada instancia recibe las variables del deployment.
En local, vercel dev no arranca daemons: construye la imagen y córrela con docker run --env-file .env.local. Si tu Redis corre en local, usa host.docker.internal en vez de localhost dentro del contenedor.
Vercel Functions vs Vercel Daemons vs un worker en Railway, Fly o VPS
La elección depende de quién inicia el trabajo y de si necesitas escalar solo. Comparativa con lo que documenta Vercel a 20 de octubre de 2026:
| Criterio | Vercel Functions + Cron | Vercel Daemons (beta) | Worker en Railway / Fly / VPS |
|---|---|---|---|
| Quién inicia el trabajo | Una petición HTTP o un cron | El propio proceso (pull) | El propio proceso (pull) |
| Duración | Limitada por invocación; waitUntil comparte el límite |
Proceso continuo | Proceso continuo |
| Escalado | Automático | Manual: instancias fijas por región, de 0 a 100 | Según el proveedor |
| Entornos | Producción y previews | Solo producción | Los que montes |
| Disco persistente | No | No (efímero) | Depende del proveedor; en un VPS, sí |
| Tráfico entrante | Sí, es su razón de ser | No acepta HTTP ni TCP entrante | Sí |
| Facturación | La de Functions | Active CPU desde 0,128 USD/h + memoria provisionada desde 0,0212 USD/GB-h (Pro) | Otra cuenta, otra factura |
| Limitación o riesgo principal | Un job largo se corta al terminar la función | Beta: sin autoescalado, sin previews, solo amd64 |
Dos plataformas que operar, logs y secretos repartidos |
La cifra que más importa es la memoria, porque se cobra por estar encendido, trabaje o no. Una instancia de 2 GiB encendida todo el mes (unas 730 horas) son unos 31 USD solo en memoria provisionada a la tarifa base de Pro (2 × 730 × 0,0212).
La Active CPU se suma aparte y solo cuenta mientras tu código ejecuta. Las tarifas son "desde" y varían por región (precios de Vercel).
Cuándo NO usar Vercel Daemons
No uses Vercel Daemons si tu carga llega a picos y necesitas que los workers la sigan. En la beta no hay autoescalado: fijas las instancias en la configuración o en el dashboard, según la página de límites. Para picos, una cola serverless con funciones como consumidoras escala mejor.
Tampoco encaja si dependes de las previews. Los daemons solo corren en producción: un cambio en el worker se prueba en local con Docker o directamente en producción.
Más límites de la beta:
- Sin almacenamiento persistente. El estado va a Vercel Blob o a una base de datos.
- Monorepos con root directory en subcarpeta (por ejemplo
apps/web) todavía no pueden añadir daemons. - Solo Linux
amd64, hasta 8 GiB y 4 vCPU por instancia. La documentación no ofrece GPU: si la necesitas, sigue fuera. - Hobby se apaga al agotar la cuota hasta el mes siguiente. A 20 de octubre, la documentación habla de 10 horas de Active CPU y la página de precios de 5 horas más 420 GB-h: comprueba la cifra.
Y el lock-in: el Dockerfile es portable; la orquestación (instancias por región, reinicios, logs) no.
Vercel Daemons para agentes de IA y workers de Temporal
Para agentes de IA, Vercel Daemons encaja porque un worker de Temporal que espera en long-poll, o un agente que espera al LLM, apenas consume Active CPU (el cifrado de las llamadas HTTPS sí cuenta, y la memoria se paga igual). Lo delicado es otro: una instancia puede pararse en cualquier momento y la documentación pide jobs idempotentes. Un agente que cobra una tarjeta a mitad de un paso necesita esa disciplina, no solo un sitio donde correr.
Repasé esos patrones en la guía para desplegar agentes de LangChain en producción, y la arquitectura completa de un agente (tools, memoria, multiagente) está en el ebook gratuito El Developer Agéntico.
Qué hacer con tu worker de colas hoy
Mira tu worker actual y pregúntate si le bastan 1-2 instancias fijas. Si es así, no escribe en disco y no lo pruebas en previews, moverlo a Vercel Daemons te quita una plataforma entera de encima. Empieza por uno poco crítico, como el de emails, y deja los pesados donde están hasta que la beta madure.
Si quieres ver cómo encaja esto en un producto completo, de la idea al despliegue, es lo que construimos en el curso Construye con IA.
Preguntas frecuentes
¿Puedo usar BullMQ con Vercel Daemons?
Sí. La documentación oficial cita BullMQ entre los consumidores soportados e incluye un daemon bullmq-worker de ejemplo. Necesitas un Redis accesible y su URL como variable de entorno de producción.
¿Cuánto cuesta Vercel Daemons?
En Pro, Active CPU desde 0,128 USD por hora y memoria provisionada desde 0,0212 USD por GB-hora, según región. Durante la beta, Pro tiene una cuota por equipo de 16 vCPU y 32 GiB funcionando a la vez. Hobby incluye una cuota mensual y, al agotarla, se detienen los daemons.
¿Vercel Daemons funciona en preview deployments?
No, durante la beta solo se ejecutan en producción. Para probar cambios, construye y corre la imagen en local con Docker, con las variables bajadas mediante vercel env pull.
¿Qué pasa con un job en curso cuando despliego una versión nueva?
Vercel envía SIGTERM a las instancias anteriores y espera 30 segundos por defecto (hasta 3.600 con terminationGracePeriodSeconds). Tu worker debe dejar de aceptar trabajo y terminar el que tiene. En BullMQ, eso es worker.close().
¿Vercel Daemons sustituye a Vercel Functions?
No. Las Functions siguen sirviendo el tráfico HTTP y los Daemons no aceptan tráfico entrante. En una app típica conviven: la función encola y el daemon procesa.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Quieres construir con agentes en serio?
Curso «Construye con IA» con Claude Code, de la idea al producto.
Developer con más de 15 años en frontend y creador de Dominicode. Escribe sobre IA aplicada al desarrollo de software.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.