CloudflareMódulo III · Procesos y servicios · Capítulo 7 de 15Nivel: BásicoEn desarrollo

7. Tareas en segundo plano

Trabajo fuera de la petición en Cloudflare: waitUntil para lo inmediato, Cron para lo programado, Queues para picos y Workflows para procesos largos.

En desarrollo. La publico antes de terminarla porque ya puede servirte; la iré completando y anotaré los cambios al final.

Víctor Rodríguez4 min de lectura

Probado conTypeScript 7.0Wrangler 4.157Vitest 5.1

Una cinta transportadora lleva tareas a un brazo robótico, con un reloj y un calendario al fondo.

Qué vas a aprender

  • Hacer trabajos pesados sin que el usuario tenga que esperar.
  • Programar tareas que se repiten solas, cada noche o cada hora.
  • Montar procesos de varios días, como un correo de bienvenida y otro de recordatorio.

Conocimientos iniciales

Si alguna de estas preguntas no te suena, ábrela: está explicada desde cero.

  • ¿Qué es una cola?

    Una lista de tareas pendientes. Tu aplicación apunta una tarea (procesar este archivo) y responde al usuario al momento; otro trozo de código la hace después y, si falla, la reintenta.

  • ¿Qué es un cron?

    Una tarea con horario: cada noche a las 3, cada 15 minutos. El nombre viene de la herramienta clásica de Unix y de su forma de escribir horarios (0 3 * * *).

  • ¿Qué es un Workflow?

    Un proceso de varios pasos que recuerda por dónde va: si un paso falla, se reintenta solo ese, y puede esperar horas o días entre pasos sin gastar.

  • ¿Qué es un Worker?Repaso

    Un trozo de tu código que Cloudflare ejecuta en sus servidores cada vez que llega una petición. Arranca en milisegundos, no tienes que mantener ninguna máquina y se paga por uso.

    Lo vimos en el capítulo 3.

Hay trabajo que no debe hacer esperar al usuario: enviar un correo de bienvenida, generar una miniatura, calcular embeddings, mandar un informe cada lunes. Cloudflare tiene cuatro formas de sacarlo de la petición, y elegir bien ahorra muchos problemas.

Para quién es y qué necesitas

Para quien tiene trabajo que no debería hacer esperar al usuario: enviar correos, procesar archivos, tareas programadas o procesos de varios días.

Antes de empezar:

  • El proyecto de los capítulos anteriores.
  • Saber que Queues y Workflows tienen plan gratuito con límites diarios; para cargas reales, el plan de pago.

Al terminar tendrás tareas después de responder, crons, una cola con reintentos y cola de mensajes muertos, y un Workflow de alta de usuario que espera días sin gastar.

Qué opción elegir

Cuál elegir
Si necesitas…Elige
Algo corto tras responder (registrar un evento, invalidar caché)Sin reintentosctx.waitUntil
Ejecutar algo con un horario (informe diario, limpieza)Cron Triggers
Muchas tareas independientes, con picos y reintentosQueues
Un proceso de varios pasos que puede tardar o esperarPor ejemplo: alta de usuario con email, espera de 3 días y recordatorioWorkflows
flowchart LR
    API[Worker API] -->|waitUntil| X[Trabajo corto]
    CR[Cron 03:00] --> P[Productor]
    API -->|send| Q[(Queue)]
    P --> Q
    Q --> C[Consumidor<br/>lotes + reintentos]
    C -->|falla 3 veces| DLQ[(Cola de<br/>mensajes muertos)]
    API -->|create| WF[Workflow]
    WF --> S1[Paso 1] --> S2[Espera] --> S3[Paso 2]

ctx.waitUntil: después de responder

src/index.ts (extracto)
app.post("/api/notes", async (c) => {
const note = await createNote(c.env.DB, "u_1", "Title", "Text");
// Responds now; this keeps running afterwards.
c.executionCtx.waitUntil(trackEvent(c.env, "note_created"));
return c.json(note, 201);
});

Cron Triggers: trabajo con horario

wrangler.jsonc (extracto)
{
"triggers": { "crons": ["0 3 * * *", "*/15 * * * *"] }
}
src/index.ts
export default {
fetch: app.fetch,
async scheduled(event, env, ctx) {
switch (event.cron) {
case "0 3 * * *":
await cleanUpTempFiles(env);
break;
case "*/15 * * * *":
await enqueuePending(env);
break;
}
},
} satisfies ExportedHandler<Env>;

Los horarios van en UTC. En el plan de pago, un cron con intervalo de una hora o más puede usar hasta 15 minutos de CPU; los más frecuentes, 30 segundos. En local se prueba con wrangler dev --test-scheduled y una petición a /__scheduled.

Queues: tareas con reintentos y lotes

Lo que pasa con cada mensaje, de principio a fin:

flowchart LR
    API[Tu API] -->|send| Q[(Cola)]
    Q -->|lote de hasta 10| C[Consumidor]
    C -->|ack| OK[Hecho]
    C -->|retry: espera 60 s| Q
    C -->|falla 3 veces| DLQ[(Cola de<br/>mensajes muertos)]
Terminal
npx wrangler queues create process-attachments
npx wrangler queues create process-attachments-dlq
wrangler.jsonc (extracto)
{
"queues": {
"producers": [
{ "binding": "ATTACHMENTS_QUEUE", "queue": "process-attachments" }
],
"consumers": [
{
"queue": "process-attachments",
"max_batch_size": 10,
"max_batch_timeout": 5,
"max_retries": 3,
"dead_letter_queue": "process-attachments-dlq"
}
]
}
}

Enviar un mensaje desde la API:

src/index.ts (extracto)
import type { AttachmentTask } from "./consumer";
await c.env.ATTACHMENTS_QUEUE.send({ key, noteId } satisfies AttachmentTask);

Consumirlo en el mismo Worker (o en otro):

src/consumer.ts
import { extractText } from "./text"; // your logic: read the PDF, the image…
export interface AttachmentTask {
key: string;
noteId: number;
}
export async function processBatch(
batch: MessageBatch<AttachmentTask>,
env: Env,
) {
for (const message of batch.messages) {
try {
await extractText(env, message.body);
message.ack();
} catch (err) {
console.error("Failed to process", message.body.key, err);
message.retry({ delaySeconds: 60 });
}
}
}
src/index.ts (extracto)
export default {
fetch: app.fetch,
queue: processBatch,
} satisfies ExportedHandler<Env, AttachmentTask>;
Ajuste Qué controla
max_batch_size / max_batch_timeout Cuántos mensajes llegan juntos y cuánto espera para juntarlos
max_retries Reintentos antes de mandar el mensaje a la cola de muertos
dead_letter_queue Dónde acaban los que fallan siempre, para revisarlos
max_concurrency Cuántos consumidores en paralelo (útil para no saturar una API externa)

Workflows: procesos de varios pasos

Un Workflow guarda el resultado de cada paso. Si el paso 3 falla, se reintenta el 3, no todo. Y puede dormir horas o días sin gastar CPU.

src/signup-flow.ts
import {
WorkflowEntrypoint,
type WorkflowEvent,
type WorkflowStep,
} from "cloudflare:workers";
import { sendEmail } from "./send";
export interface Signup {
userId: string;
email: string;
}
export class SignupFlow extends WorkflowEntrypoint<Env, Signup> {
async run(event: WorkflowEvent<Signup>, step: WorkflowStep) {
const { userId, email } = event.payload;
await step.do("send welcome", async () => {
await sendEmail(this.env, email, "welcome");
});
await step.sleep("wait 3 days", "3 days");
const active = await step.do("check activity", async () => {
const row = await this.env.DB
.prepare("SELECT COUNT(*) AS n FROM notes WHERE user_id = ?")
.bind(userId)
.first<{ n: number }>();
return (row?.n ?? 0) > 0;
});
if (!active) {
await step.do(
"send reminder",
{ retries: { limit: 5, delay: "1 minute", backoff: "exponential" } },
async () => sendEmail(this.env, email, "reminder"),
);
}
}
}
wrangler.jsonc (extracto)
{
"workflows": [
{ "name": "signup-flow", "binding": "SIGNUP_FLOW", "class_name": "SignupFlow" }
]
}
src/index.ts (extracto)
export { SignupFlow } from "./signup-flow";
import type { Signup } from "./signup-flow";
app.post("/api/signup", async (c) => {
const { userId, email } = await c.req.json<Signup>();
const instance = await c.env.SIGNUP_FLOW.create({
params: { userId, email },
});
return c.json({ id: instance.id }, 202);
});

Cuánto cuesta

Cuánto cuesta al mes

Supuestos: 3 millones de mensajes en colas y 20.000 altas con un Workflow de 4 pasos

ConceptoImporte
QueuesCada mensaje son unas 3 operaciones (escribir, leer, borrar): 9 M; 1 M incluida; 8 M × 0,40 $2,40 $
Workflows: pasos80.000 pasos; 500.000 incluidos al mes0,00 $
Cron TriggersSe cobran como peticiones de Workers0,00 $
Total al mes2,40 $

Precios consultados el 3 de noviembre de 2026. Pueden cambiar.

Producto Gratuito Pago (incluido / después)
Queues 10.000 operaciones al día, 24 h de retención 1 M al mes; 0,40 $ por millón. Retención hasta 14 días
Workflows 3.000 pasos al día 500.000 pasos al mes; 0,80 $ por cada 100.000
Cron Triggers 5 por cuenta 250 por cuenta

Resumen

  • waitUntil para lo trivial, Cron para el horario, Queues para el volumen y Workflows para los procesos con pasos.
  • El cron encola; el consumidor trabaja.
  • Procesado idempotente siempre: los mensajes pueden repetirse.
  • Siguiente capítulo: enviar y recibir email.

Recursos

¿Te ha servido? Esto forma parte de un libro que estoy escribiendo: Cloudflare de cero a producción. Ver el índice provisional →