Una de las reglas más útiles en backend:
Si el usuario no necesita el resultado inmediato, no bloquees la request esperando ese trabajo.
Ejemplos típicos:
- enviar un email
- generar un recibo
- procesar una imagen
- llamar un webhook externo
- actualizar un CRM
- guardar analytics
- sincronizar con otro sistema
Cloudflare Queues te ayuda a separar dos caminos:
experiencia del usuario -> request rápida
procesamiento interno -> mensajes en cola
Si vienes de Workers, la idea es: el Worker sigue siendo la entrada; la Queue se lleva el trabajo que no debe vivir dentro del fetch.
Términos (déjalo claro, no lo inventes)
| Término | Significado |
|---|---|
| Queue | cola de mensajes |
| Message | unidad de trabajo |
| Producer | código que manda mensajes |
| Consumer | código que lee y procesa |
| Retry | reintento controlado |
| DLQ | Dead Letter Queue — cola para mensajes que agotaron retries |
Flujo base:
request -> producer -> Queue -> consumer
Modelo mental: checkout
Cuando alguien compra, tu sistema puede necesitar:
- confirmar el request
- responder al usuario
- mandar recibo
- actualizar CRM
- registrar analytics
- avisar a soporte
No todo eso tiene que ocurrir antes de responder.
Puedes devolver:
{ "accepted": true }
y dejar jobs en una Queue para después.
Producer y consumer en Workers
Producer desde fetch():
type ReceiptJob = {
orderId: string;
email: string;
};
export interface Env {
RECEIPT_QUEUE: Queue<ReceiptJob>;
}
export default {
async fetch(request, env): Promise<Response> {
const job = await request.json<ReceiptJob>();
await env.RECEIPT_QUEUE.send({
orderId: job.orderId,
email: job.email,
});
return Response.json({ accepted: true }, { status: 202 });
},
} satisfies ExportedHandler<Env>;
Consumer:
export default {
async queue(batch, env, ctx): Promise<void> {
for (const message of batch.messages) {
try {
await sendReceipt(message.body);
message.ack();
} catch (error) {
console.error("receipt_failed", {
orderId: message.body.orderId,
error,
});
message.retry({ delaySeconds: 30 });
}
}
},
} satisfies ExportedHandler<Env, ReceiptJob>;
Concepto:
fetch produce
queue consume
RECEIPT_QUEUE existe como binding. El tipo del message es parte del diseño, no un detalle cosmético.
Por qué a menudo 202 Accepted
202 comunica:
Recibí la solicitud y la acepté para procesamiento; el trabajo completo puede seguir pendiente.
No siempre hace falta. Para flujos async suele ser más honesto que fingir un 200 OK cuando el recibo, el CRM y el webhook aún no corrieron.
Retries: los errores no desaparecen
Una Queue no elimina fallos. Los saca del camino principal y te da un mecanismo para reintentar.
Los sistemas externos fallan:
- email caído
- CRM lento
- webhook con timeout
- API con rate limit
- payload temporalmente inválido
Si todo eso vive dentro de la request, el usuario paga el costo. Con una Queue, puedes responder rápido y procesar con más control.
DLQ: no es basurero, es evidencia
DLQ = Dead Letter Queue: mensajes que llegaron al límite de retries.
No la pienses como “donde cae lo malo”.
Piensa:
bandeja de investigación para mensajes que no pudieron procesarse.
Una DLQ te ayuda a responder:
- qué payload falló
- cuántas veces
- qué consumer lo tocó
- qué error se lanzó
- si se puede corregir y reprocesar
Sin DLQ, muchos fallos async quedan invisibles o se pierden.
Idempotencia: asume re-entrega
En sistemas async debes asumir que un mensaje puede procesarse más de una vez.
Idempotente:
procesar el mismo mensaje dos veces no debe duplicar un efecto peligroso.
Malo:
send receipt
Mejor:
send receipt for orderId if receipt_sent = false
O:
add CRM event with idempotency key = jobId
No basta con “usar Queue”. Hay que diseñar el consumer.
Cuándo usaría Queues
Buenas señales:
- el usuario no necesita el resultado final inmediato
- quieres absorber picos
- quieres desacoplar servicios
- quieres batches
- una API externa puede fallar
- necesitas retries, delays o una DLQ
Patrones comunes:
checkout -> Queue -> receipt email
upload -> Queue -> image resize
webhook -> Queue -> normalization
form -> Queue -> CRM sync
analytics-> Queue -> batch write
Cuándo no metería todo en una Queue
Puede no ser el patrón correcto si:
- el usuario necesita el resultado final ahora
- necesitas una transacción fuerte entre varios sistemas
- el proceso tiene muchos pasos durables y pausas largas
- necesitas coordinación por entidad en tiempo real
| Problema | Mejor herramienta |
|---|---|
| Multi-step durable | Workflows |
| Estado por entidad | Durable Objects |
| Request simple | un Worker basta |
Checklist de producción
- Define el tipo del message.
- Incluye
jobId,orderIdu otra key estable. - Decide qué errores se reintentan.
- Decide qué errores van a DLQ.
- Haz el consumer idempotente.
- Loguea con contexto.
- Mide backlog, retries y fallos.
- Documenta quién produce y quién consume.
Una Queue sin observabilidad es una caja negra. Con logs y DLQ se vuelve herramienta operativa.
La frase para recordar
Worker fetch = producer
Queue = buffer de mensajes
queue handler = consumer
retry = reintento controlado
DLQ = investigación (y posible reproceso)
Responde rápido. Procesa después. Observa siempre.
Serie Cloudflare para builders: Workers · Bindings · Durable Objects · Queues
Docs: Queues · How Queues works · Getting started · Batching, retries, delays · Dead Letter Queues