Ejecución durable: flujos que sobreviven a reinicios
Por qué las colas y las máquinas de estado fallan en procesos de varios pasos, y cómo la ejecución durable con Temporal sobrevive a caídas y despliegues.

Todo backend termina topándose con un proceso que no cabe en una sola petición HTTP. Un flujo de cumplimiento de pedidos que cobra una tarjeta, reserva inventario, notifica a un almacén y envía un correo de confirmación. Un pipeline de onboarding que provisiona recursos en la nube, semilla una base de datos y envía una secuencia de bienvenida. Estos flujos duran de segundos a minutos, tocan varios servicios externos y deben completarse de forma confiable — incluso si el servidor se cae a mitad de camino.
La respuesta estándar es una cola de trabajos más una máquina de estados persistida en una base de datos. Funciona, hasta que la máquina de estados crece a 15 estados, 40 transiciones y todos los desarrolladores del equipo le tienen miedo. Hay un modelo mejor: la ejecución durable.
El problema de los trabajos ad hoc de varios pasos
Una cola de trabajos maneja bien unidades discretas de trabajo. Se desmorona cuando un solo “trabajo” es en realidad una secuencia de pasos dependientes con lógica de ramificación, esperas externas y requisitos de rollback.
// ❌ Fragile — state lives in ephemeral memory; server restart = lost progress
async function processOrder(orderId: string) {
await chargePayment(orderId);
await reserveInventory(orderId); // If the process crashes here...
await sendConfirmationEmail(orderId); // ...this line never runs
}
// ✅ Durable — each step is checkpointed; restarts replay from the last successful step
export async function processOrderWorkflow(orderId: string): Promise<void> {
await workflow.executeActivity(chargePayment, { args: [orderId] });
await workflow.executeActivity(reserveInventory, { args: [orderId] });
await workflow.executeActivity(sendConfirmationEmail, { args: [orderId] });
}La diferencia no es solo la lógica de reintentos. Es que el motor del flujo de trabajo persiste el historial de ejecución para que un worker reiniciado pueda reconstruir exactamente dónde estaba el flujo sin volver a ejecutar los pasos completados.
Qué significa realmente la ejecución durable
En Temporal (y motores similares como Inngest o Restate), el código del flujo de trabajo no se ejecuta directamente contra sistemas externos. En cambio, el motor registra cada decisión y resultado de actividad como un historial de eventos solo de adición. Cuando un worker toma un flujo de trabajo, reproduce ese historial para reconstruir el estado — avanzando rápido por encima de las actividades completadas.
Esto significa que tu función de flujo de trabajo corre en un sandbox determinista. De aquí siguen algunas reglas:
- Sin E/S directa dentro del código del flujo de trabajo (
fetch,fs, consultas a base de datos) - No uses
Date.now()niMath.random()— usaworkflow.now()yworkflow.random()en su lugar - Todos los efectos secundarios ocurren en actividades, que son funciones async normales que corren fuera del sandbox
El modelo mental: el código del flujo de trabajo es un coordinador que llama actividades, espera timers o señales y toma decisiones basadas en los resultados de las actividades.
Modelando flujos de trabajo en TypeScript
El SDK de TypeScript de Temporal te permite escribir flujos de trabajo como funciones async comunes.
import * as workflow from "@temporalio/workflow";
import type { OrderActivities } from "./activities";
const { chargePayment, reserveInventory, sendConfirmationEmail, refundPayment } =
workflow.proxyActivities<OrderActivities>({
startToCloseTimeout: "30 seconds",
retry: {
maximumAttempts: 3,
nonRetryableErrorTypes: ["PaymentDeclinedError"],
},
});
export async function processOrderWorkflow(orderId: string): Promise<string> {
let paymentCharged = false;
try {
await chargePayment(orderId);
paymentCharged = true;
await reserveInventory(orderId);
await sendConfirmationEmail(orderId);
return "fulfilled";
} catch (err) {
// Compensate only if payment already completed
if (paymentCharged) {
await refundPayment(orderId);
}
throw err;
}
}Las actividades viven en un módulo aparte y no tienen restricciones — pueden acceder a la base de datos, llamar APIs o escribir archivos:
// activities.ts — ordinary async functions, no sandbox constraints
export const orderActivities = {
async chargePayment(orderId: string): Promise<void> {
const order = await db.orders.findOrThrow(orderId);
await stripe.paymentIntents.capture(order.paymentIntentId);
await db.orders.update(orderId, { chargedAt: new Date() });
},
async reserveInventory(orderId: string): Promise<void> {
const items = await db.orderItems.findAll({ orderId });
await inventoryService.reserveBatch(items);
},
};
export type OrderActivities = typeof orderActivities;La separación parece ceremonial hasta que necesitas mockear actividades en pruebas o cambiar implementaciones sin tocar la lógica del flujo de trabajo.
Señales y consultas: interactuar con flujos de trabajo en ejecución
Los flujos de trabajo de larga duración a menudo necesitan entrada externa en medio de la ejecución — esperando una aprobación humana, un callback de webhook o una confirmación de pago de un proveedor externo.
import * as workflow from "@temporalio/workflow";
// Define signal and query handlers
const approveSignal = workflow.defineSignal<[{ approvedBy: string }]>("approve");
const rejectSignal = workflow.defineSignal<[{ reason: string }]>("reject");
const statusQuery = workflow.defineQuery<string>("status");
export async function expenseApprovalWorkflow(expenseId: string): Promise<void> {
let status = "pending";
let approvedBy: string | null = null;
let rejectionReason: string | null = null;
workflow.setHandler(approveSignal, ({ approvedBy: by }) => {
status = "approved";
approvedBy = by;
});
workflow.setHandler(rejectSignal, ({ reason }) => {
status = "rejected";
rejectionReason = reason;
});
workflow.setHandler(statusQuery, () => status);
// Block until approved or rejected, or timeout after 7 days
await workflow.condition(() => status !== "pending", "7 days");
if (status === "approved" && approvedBy) {
await processApprovedExpense({ expenseId, approvedBy });
} else {
await notifyRejection({ expenseId, reason: rejectionReason ?? "Timed out" });
}
}Las señales son mensajes de enviar y olvidar enviados a un flujo de trabajo en ejecución. Las consultas devuelven el estado actual sin avanzar el flujo de trabajo. Ambos se envían desde el código de la aplicación mediante el cliente de Temporal — sin polling, sin tabla de estado separada.
workflow.condition() es cómo bloqueas un flujo de trabajo hasta que llegue un evento externo. Por debajo es solo un timer + verificación de condición reproducida desde el historial. Ningún thread queda realmente bloqueado.
Manejo de errores y compensación
La ejecución durable no elimina los fallos de los sistemas distribuidos — te da las herramientas para manejarlos limpiamente. Temporal reintenta las actividades automáticamente ante fallos transitorios. Para fallos no transitorios, el patrón saga se mapea naturalmente al código del flujo de trabajo.
export async function provisionTenantWorkflow(tenantId: string): Promise<void> {
const provisioned: string[] = [];
try {
await createDatabase(tenantId);
provisioned.push("database");
await createStorageBucket(tenantId);
provisioned.push("bucket");
await deployAppInstance(tenantId);
provisioned.push("app");
await sendWelcomeEmail(tenantId);
} catch (err) {
// Compensate in reverse order
const rollbacks = provisioned.reverse().map((resource) => {
if (resource === "app") return teardownAppInstance(tenantId);
if (resource === "bucket") return deleteStorageBucket(tenantId);
if (resource === "database") return dropDatabase(tenantId);
});
// Activities can also fail — Temporal retries them independently
await Promise.allSettled(rollbacks);
throw err;
}
}Compáralo con implementar la misma lógica de compensación con una máquina de estados en base de datos. Cada transición, cada camino de rollback, cada reintento necesita actualizar una fila. La versión del flujo de trabajo se lee como el camino feliz con manejo de errores explícito — que es exactamente lo que es.
Cuándo no usar ejecución durable
La ejecución durable tiene costos reales. El servidor de Temporal es otra pieza de infraestructura que operar. Cada resultado de actividad se serializa y almacena — payloads grandes o flujos de alto throughput pueden presionar el almacén de historial.
| Caso de uso | Recomendación |
|---|---|
| Trabajo en segundo plano de un solo paso | BullMQ o la cola nativa son más simples |
| Requisitos de latencia menores a un segundo | El overhead del flujo de trabajo agrega decenas de ms como mínimo |
| Fan-out sin estado (exportaciones por lotes) | Mejor usar un map sobre una cola de trabajos |
| Flujo humano en el bucle de varios pasos y varios días | Encaja muy bien |
| Saga distribuida con lógica de compensación | Encaja muy bien |
| Procesos que esperan callbacks externos | Encaja muy bien |
El patrón de señales solo ya vale la pena considerar Temporal para cualquier flujo que se queda esperando un webhook. Hacer polling a una columna de estado es un problema resuelto que genera carga innecesaria en la base de datos y aun así requiere manejar cuidadosamente los timeouts.
Conclusiones clave
- La ejecución durable no es solo lógica de reintentos — es un historial de eventos reproducido que reconstruye el estado del flujo de trabajo entre caídas y despliegues sin volver a ejecutar los pasos completados.
- El código del flujo de trabajo es un coordinador, no un ejecutor — toda la E/S ocurre en actividades; las funciones de flujo deben ser deterministas.
- Las señales y consultas reemplazan el polling de estado — envía una señal para desbloquear un flujo de trabajo en espera, consúltalo por su estado actual, sin tocar la base de datos.
- La lógica de compensación se lee como código, no como migraciones — los patrones saga se mapean directamente a bloques try/catch en funciones de flujo de trabajo.
- Evalúa el ajuste antes de adoptar — trabajos simples en segundo plano, requisitos de latencia menores a un segundo y fan-out sin estado se resuelven mejor con colas tradicionales.
- El costo de infraestructura es real — Temporal (o un equivalente administrado como Temporal Cloud) es el tradeoff correcto para flujos que abarcan horas o días, no para cada tarea asíncrona.


