<!-- Canonical: https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution
Language: es
Content-SHA256: 8ca21e98cb30b6fbb9c6d359419657d7bf3db215348af5350f1650390bcf4d7d
-->

Backend

# Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja

2026-02-2810 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/temporal-workflow-engine-durable-execution>)

## Definición de Ejecución Durable

La idea central detrás de [Temporal](<https://temporal.io/>) es simple de enunciar y transformadora en la práctica: **el código se persiste**. No solo los datos, no solo el estado, el progreso real de ejecución de la función se graba para que si el proceso crashea, pueda resumir exactamente donde quedó.

Aquí va un workflow de Temporal en TypeScript:

```typescript
import { proxyActivities, sleep, ApplicationFailure } from '@temporalio/workflow';
import type * as activities from './activities';

const { chargePayment, reserveInventory, shipOrder, sendConfirmation, refundPayment, releaseInventory } =
 proxyActivities<typeof activities>({
 startToCloseTimeout: '30s',
 retry: {
 maximumAttempts: 3,
 backoffCoefficient: 2,
 initialInterval: '1s',
 },
 });

export async function processOrder(order: OrderInput): Promise<OrderResult> {
 // Paso 1: Reservar inventario
 const reservation = await reserveInventory(order.items);

 // Paso 2: Cobrar pago
 let paymentId: string;
 try {
 paymentId = await chargePayment(order.paymentMethod, order.total);
 } catch (err) {
 // Compensación: liberar inventario si falla el pago
 await releaseInventory(reservation.id);
 throw ApplicationFailure.nonRetryable(
 `Pago falló: ${err.message}`
 );
 }

 // Paso 3: Enviar la orden
 let shipmentId: string;
 try {
 shipmentId = await shipOrder(reservation.id, order.shippingAddress);
 } catch (err) {
 // Compensación: reembolsar pago y liberar inventario
 await refundPayment(paymentId);
 await releaseInventory(reservation.id);
 throw ApplicationFailure.nonRetryable(
 `Envío falló: ${err.message}`
 );
 }

 // Paso 4: Enviar confirmación
 await sendConfirmation(order.email, {
 orderId: order.id,
 paymentId,
 shipmentId,
 items: order.items,
 });

 return { orderId: order.id, paymentId, shipmentId, status: 'completed' };
}
```

Observar este código. Es simplemente una función async regular con bloques try-catch. Sin máquinas de estado. Sin serialización de estados intermedios a una base de datos. Sin lógica de reintentos wrapeada alrededor de cada llamada externa. Sin manejo de dead-letter queues.

Pero aquí va lo que Temporal hace detrás de escena:

1. Cuando se llama ` reserveInventory `, Temporal graba un evento "ScheduleActivityTask" en el historial de eventos del workflow
2. Cuando la activity completa, se graba un evento "ActivityTaskCompleted" con el resultado
3. Si el proceso del worker crashea entre los pase es 2 y 3 (pago), un nuevo worker levanta el workflow
4. Temporal **hace replay** de la función del workflow desde el principio, pero en vez de re-ejecutar ` reserveInventory `, lee el resultado grabado del historial de eventos
5. El replay llega al punto de falla y continúa ejecutando los pase es restantes

Esto es lo que significa "ejecución durable". La función del workflow parece correr continuamente, pero en realidad es una serie de pase es grabados que pueden ser replayeados en cualquier worker. El ` await ` en cada activity es un punto de persistencia.

## La Restricción de Determinismo

Hay una restricción fundamental que hace que esto funcione: **las funciones de workflow deben ser determinísticas**. Dados los mismos inputs y los mismos resultados de activities, el workflow debe tomar la misma secuencia de decisiones.

Esto significa que no es posible hacer lo siguiente dentro de una función de workflow:

```typescript
// TODO ESTO VA A ROMPER EL REPLAY

// 1. Tiempo no-determinístico
const now = Date.now(); // ¡Diferente en replay!
// Utilizar en cambio: workflow.now()

// 2. Números random
const id = Math.random().toString(36); // ¡Diferente en replay!
// Utilizar en cambio: workflow.uuid4()

// 3. I/O directo
const data = await fetch('https://api.example.com/data'); // ¡Efecto secundario!
// Utilizar en cambio: una activity

// 4. Iteración no-determinística
const keys = Object.keys(someMap); // ¡Orden no garantizado!
// Utilizar en cambio: keys ordenadas o arrays
```

Esta restricción inicialmente se siente limitante. Con el uso extendido, se revela como un feature. Fuerza una separación limpia entre **decisiones** (workflow) y **efectos** (activities). El workflow es lógica pura. Las activities son donde pasa toda la interacción sucia con el mundo real.

Las activities son funciones regulares que pueden hacer cualquier cosa, llamadas HTTP, queries a bases de datos, I/O de archivos, llamadas a APIs de terceros. Corren en workers, y Temporal maneja reintentos, timeouts y heartbeating:

```typescript
// activities.ts, estas corren en workers, NO en el sandbox del workflow
import { Context } from '@temporalio/activity';

export async function chargePayment(
 method: PaymentMethod,
 amount: number
): Promise<string> {
 // Heartbeat para operaciones largas, le avisa a Temporal que estamos vivos
 Context.current().heartbeat('cobrando');

 const result = await stripe.charges.create({
 amount: Math.round(amount * 100),
 currency: 'usd',
 source: method.token,
 });

 return result.id;
}

export async function shipOrder(
 reservationId: string,
 address: ShippingAddress
): Promise<string> {
 Context.current().heartbeat('creando envío');

 const shipment = await shippingProvider.createShipment({
 reservationId,
 address,
 service: 'standard',
 });

 return shipment.trackingNumber;
}
```

## Patrón Saga: Compensación Hecha Bien

El workflow de procesamiento de órdenes de arriba implementa el patrón saga, una secuencia de transacciones con acciones compensatorias si algún paso falla. En un sistema típico basado en RabbitMQ, el coordinador de sagas es su propio servicio con su propia base de datos, trackeando el estado de cada instancia de saga.

Con Temporal, el patrón saga son simplemente bloques try-catch. Pero para workflows más complejos con muchos pasos, un helper que acumula compensaciones es útil:

```typescript
class SagaCompensation {
 private compensations: Array<() => Promise<void>> = [];

 addCompensation(fn: () => Promise<void>): void {
 this.compensations.unshift(fn); // LIFO, compensar en orden inverso
 }

 async compensate(): Promise<void> {
 for (const compensation of this.compensations) {
 try {
 await compensation();
 } catch (err) {
 // Loguear pero continuar, compensación parcial es mejor que nada
 console.error('Compensación falló:', err);
 }
 }
 }
}

export async function processComplexOrder(order: ComplexOrder): Promise<void> {
 const saga = new SagaCompensation();

 try {
 const reservation = await reserveInventory(order.items);
 saga.addCompensation(() => releaseInventory(reservation.id));

 const paymentId = await chargePayment(order.payment, order.total);
 saga.addCompensation(() => refundPayment(paymentId));

 const discountApplied = await applyLoyaltyDiscount(order.customerId, order.total);
 saga.addCompensation(() => revertLoyaltyDiscount(order.customerId, discountApplied));

 const shipmentId = await shipOrder(reservation.id, order.address);
 saga.addCompensation(() => cancelShipment(shipmentId));

 await updateOrderStatus(order.id, 'completed');
 await sendConfirmation(order.email, { reservation, paymentId, shipmentId });

 } catch (err) {
 await saga.compensate();
 await updateOrderStatus(order.id, 'failed');
 throw err;
 }
}
```

Este es el mismo patrón que un coordinador de sagas de 3.000 líneas implementa, en unas 50 líneas. Y es confiable, si un worker crashea durante la compensación, Temporal hace replay del workflow y la compensación continúa desde donde quedó.

## Señales, Queries, y Human-in-the-Loop

Los workflows de Temporal pueden recibir señales externas (eventos que modifican el estado del workflow) y responder a queries (inspección read-only del estado del workflow). Esto es increíblemente poderoso para workflows que necesitan aprobación humana o input externo:

```typescript
import { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';

const approveSignal = defineSignal<[{ approvedBy: string; notes: string }]>('approve');
const rejectSignal = defineSignal<[{ rejectedBy: string; reason: string }]>('reject');
const statusQuery = defineQuery<OrderStatus>('status');

export async function orderWithApproval(order: OrderInput): Promise<OrderResult> {
 let status: OrderStatus = 'pending_approval';
 let approval: { approvedBy: string; notes: string } | null = null;
 let rejection: { rejectedBy: string; reason: string } | null = null;

 setHandler(approveSignal, (data) => {
 approval = data;
 status = 'approved';
 });

 setHandler(rejectSignal, (data) => {
 rejection = data;
 status = 'rejected';
 });

 setHandler(statusQuery, () => status);

 // Esperar aprobación o rechazo, con timeout
 const approved = await condition(
 () => approval !== null || rejection !== null,
 '24h' // Timeout después de 24 horas
 );

 if (!approved || rejection) {
 status = 'rejected';
 await notifyRejection(order.email, rejection?.reason ?? 'Timeout de aprobación');
 return { orderId: order.id, status: 'rejected' };
 }

 status = 'processing';
 // ... resto del workflow de la orden
}
```

Desde afuera, le se envía una señal al workflow:

```typescript
const handle = client.workflow.getHandle('order-12345');
await handle.signal(approveSignal, {
 approvedBy: 'manager@company.com',
 notes: 'Aprobado para cliente VIP',
});
```

El workflow puede sentarse esperando aprobación por horas, días o semanas. El worker no mantiene una conexión abierta. No pollea. El estado del workflow está persistido en la base de datos de Temporal. Cuando la señal llega, Temporal agenda el workflow para ejecución, un worker lo levanta, hace replay del historial, procesa la señal, y continúa.

## Arquitectura de Producción

Un setup de producción corre Temporal Server en Kubernetes con un backend PostgreSQL:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: temporal-server
spec:
 replicas: 3
 template:
 spec:
 containers:
 - name: temporal-server
 image: temporalio/server:1.25.2
 env:
 - name: DB
 value: postgresql
 - name: POSTGRES_HOST
 value: temporal-postgres.db.svc.cluster.local
 - name: NUM_HISTORY_SHARDS
 value: "512"
 ports:
 - containerPort: 7233
 - containerPort: 7234
 - containerPort: 7235
 - containerPort: 7239
```

Los workers corren como deployments separados:

```typescript
// worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';

async function run() {
 const worker = await Worker.create({
 workflowsPath: require.resolve('./workflows'),
 activities,
 taskQueue: 'order-processing',
 maxConcurrentActivityTaskExecutions: 100,
 maxConcurrentWorkflowTaskExecutions: 50,
 stickyQueueScheduleToStartTimeout: '10s',
 });

 await worker.run();
}

run().catch((err) => {
 console.error(err);
 process.exit(1);
});
```

## Componentes Reemplazados y Resultados Medidos

Antes de Temporal:

- 7 colas de RabbitMQ, 4 dead-letter queues
- 12 servicios consumidores (Node.js)
- 1 servicio coordinador de sagas (3.000 líneas)
- 1 base de datos PostgreSQL para estado de sagas
- Lógica de reintentos custom en cada consumidor
- Scripts de compensación manuales para sagas trabadas

Después de Temporal:

- 1 cluster de Temporal (3 nodos)
- 3 deployments de workers (orden, pago, envío)
- ~800 líneas de código de workflow
- ~1.200 líneas de código de activities
- Cero lógica de reintentos custom
- Cero scripts de compensación manuales

El impacto operacional:

- **Sagas trabadas/huérfanas**: Pasaron de ~15/semana a cero. Temporal no pierde el rastro de los workflows.
- **Incidentes de doble cobro**: Pasaron de ~3/mes a cero. El patrón saga con ejecución durable realmente garantiza que la compensación se ejecute.
- **Tiempo medio para diagnosticar problemas**: Bajó de 2-3 horas (correlacionar logs entre 12 servicios) a 5-10 minutos (mirar el historial de eventos del workflow en la UI de Temporal).
- **Alertas de guardia**: Bajaron un 70%. La mayoría de las alertas viejas eran por consumidores trabados o backlogs en las colas.

El historial de eventos en la UI de Temporal es genuinamente transformador para debugging. Cada llamada a activity, cada resultado, cada reintento, cada señal, está todo ahí en orden cronológico. Cuando un cliente reporta un problema, el workflow se puede buscar por ID de orden, mostrando exactamente qué pasó, cuándo, y por qué.

## Cuándo No Usar Temporal

Temporal no es la herramienta correcta para todo:

**APIs simples de request-response**: Si tu endpoint llama a una base de datos y devuelve un resultado, no se necesita un workflow engine. El overhead de grabar eventos y hacer replay del historial es desperdiciado.

**Operaciones de alta frecuencia y baja latencia**: Temporal agrega ~20-50ms de overhead por workflow task. Para operaciones que necesitan latencia sub-milisegundo, utilizar llamadas directas entre servicios.

**Event streaming puro**: Si se necesita broadcastear eventos a muchos consumidores (fan-out), Kafka es mejor. Temporal es para orquestar una secuencia específica de pasos, no para broadcastear.

**Workflows con payloads enormes**: Temporal almacena los inputs y outputs de activities en su historial de eventos. Si tus activities pasan blobs de 10MB, tu historial va a ser enorme. Pasar referencias (keys de S3, IDs de base de datos) en vez de datos.

El punto óptimo es cualquier proceso multi-paso que abarca múltiples servicios, necesita reintentos y compensación, y donde la correctitud importa más que el throughput crudo. Procesamiento de órdenes, flujos de pago, secuencias de onboarding de usuarios, orquestación de pipelines de datos, workflows de aprobación, ahí es donde Temporal brilla.

Para un pipeline típico de procesamiento de órdenes, la migración toma unas seis semanas incluyendo testing. Típicamente se pasa más tiempo descomisionando la infraestructura vieja que construyendo los nuevos workflows. Eso revela dónde realmente vive la complejidad.

## Preguntas frecuentes

### Cómo se compara Temporal con AWS Step Functions?

Step Functions usa una definición de state machine basada en JSON (ASL), mientras que Temporal te deja escribir workflows como código regular en Go, TypeScript, Java o Python. Temporal te da más flexibilidad para lógica compleja (loops, condicionales, sub-workflows) y no tiene el límite de 25K eventos en el historial. Step Functions es más fácil para workflows lineales simples y tiene integración más ajustada con AWS.

### Temporal puede reemplazar a Kafka para arquitecturas event-driven?

No directamente. Temporal es para orquestación, coordinar una secuencia de pase es con reintentos y compensación. Kafka es para event streaming, broadcastear eventos a múltiples consumidores independientes. Son complementarios. Temporal puede orquestar workflows que se disparan por eventos de Kafka, combinando las fortalezas de ambos.

### Qué pasa con los workflows corriendo cuando deployeo código nuevo?

Temporal hace replay del historial de eventos del workflow contra tu código nuevo. Si tu código nuevo es deterministic-compatible con el viejo (misma secuencia de llamadas a activities, timers, etc.), el replay tiene éxito y el workflow continúa. Si cambiaste la secuencia, el replay falla con un error de no-determinismo. Utilizar workflow.patched() o versionado de task queues para manejar esto.

## Tips

1. **Tip:** No invocar a ` Date.now() ` o ` Math.random() ` dentro de una función de workflow de Temporal. Utilizar ` workflow.now() ` y el random determinístico de ` @temporalio/workflow `. Los replays van a divergir silenciosamente, y te se va a enterar cuando un workflow falle en recuperarse después de un restart del worker, generalmente a las 3 de la mañana.
2. **Tip:** Configurar ` startToCloseTimeout ` y ` scheduleToCloseTimeout ` en cada activity. Las activities pueden colgarse para siempre en llamadas a APIs externas sin timeout, lo que significa que el workflow también espera para siempre. Establecer 30 segundos por defecto para llamadas a API, 5 minutos para operaciones batch, y siempre agregar un ` heartbeatTimeout ` para activities de larga duración.
3. **Tip:** Utilizar ` workflow.patched() ` para versionado en vez de intentar deployar cambios breaking en workflows. Los workflows corriendo van a hacer replay con el code path viejo mientras los nuevos usan la versión parcheada. Un intento de migración big-bang causó que 400 workflows in-flight fallaran todos simultáneamente un lunes a la mañana.
4. **Tip:** Ejecutar el CLI ` tctl ` de Temporal con ` workflow describe -w <id> ` antes de debuggear cualquier cosa en tu código. La mitad de las veces el problema es una retry policy mal configurada o un timeout demasiado agresivo, no tu lógica de negocio.

- temporal
- durable-execution
- workflow-engine
- saga-pattern
- orchestration
- distributed-systems
- message-queues
- backend-architecture

[Volver al Blog](<https://lucioduran.com/es/blog>)
