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
| Si necesitas… | Elige |
|---|---|
| Algo corto tras responder (registrar un evento, invalidar caché)Sin reintentos | ctx.waitUntil |
| Ejecutar algo con un horario (informe diario, limpieza) | Cron Triggers |
| Muchas tareas independientes, con picos y reintentos | Queues |
| Un proceso de varios pasos que puede tardar o esperarPor ejemplo: alta de usuario con email, espera de 3 días y recordatorio | Workflows |
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
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
{ "triggers": { "crons": ["0 3 * * *", "*/15 * * * *"] }}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)]
npx wrangler queues create process-attachmentsnpx wrangler queues create process-attachments-dlq{ "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:
import type { AttachmentTask } from "./consumer";
await c.env.ATTACHMENTS_QUEUE.send({ key, noteId } satisfies AttachmentTask);Consumirlo en el mismo Worker (o en otro):
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 }); } }}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.
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"), ); } }}{ "workflows": [ { "name": "signup-flow", "binding": "SIGNUP_FLOW", "class_name": "SignupFlow" } ]}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
| Concepto | Importe |
|---|---|
| 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 mes | 0,00 $ |
| Cron TriggersSe cobran como peticiones de Workers | 0,00 $ |
| Total al mes | 2,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
waitUntilpara 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.
