The Replay Model: How AWS Lambda Durable Functions Actually Work

Published: (December 2, 2025 at 07:27 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

The Core Principle

Your handler function re‑executes from the beginning on every invocation, but completed operations return cached results from checkpoints instead of re‑executing.

async function processOrder(event: any, ctx: DurableContext) {
  const order = await ctx.step('create-order', async () => {
    console.log('Creating order...');
    return { orderId: '123', total: 50 };
  });

  const payment = await ctx.step('process-payment', async () => {
    console.log('Processing payment...');
    return { transactionId: 'txn-456', status: 'success' };
  });

  await ctx.wait({ seconds: 300 }); // Wait 5 minutes

  const notification = await ctx.step('send-notification', async () => {
    console.log('Sending notification...');
    return { sent: true };
  });

  return { order, payment, notification };
}

Invocation Flow

Invocation 1 (t = 0 s)

Creating order...
Processing payment...
[Checkpoint: create-order completed]
[Checkpoint: process-payment completed]
[Function terminates – waiting 5 minutes]

Invocation 2 (t = 300 s, after wait completes)

[REPLAY MODE: Skipping create-order – returning cached result]
[REPLAY MODE: Skipping process-payment – returning cached result]
[EXECUTION MODE: Running send-notification]
Sending notification...
[Checkpoint: send-notification completed]
[Function completes]

The function starts from the beginning each time, but previously completed steps are skipped, so logs appear only once.

Execution Modes: The Secret Sauce

The SDK operates in two automatic modes:

  • ExecutionMode – first‑time execution of operations; results are saved to checkpoints, logs are emitted, and side effects occur.
  • ReplayMode – replay of previously completed operations; cached results are returned instantly, logs are suppressed, and no side effects happen.

The SDK switches from ReplayMode to ExecutionMode when it reaches an operation that has not yet been completed.

How Checkpoints Work

Each operation creates a checkpoint that stores metadata and the result:

{
  "operationId": "2",
  "operationType": "STEP",
  "operationName": "process-payment",
  "status": "SUCCEEDED",
  "result": {
    "transactionId": "txn-456",
    "status": "success"
  }
}

When the function restarts, the SDK loads all checkpoints, indexes them by operation ID, returns cached results for completed operations, and executes new operations normally.

The Determinism Requirement

Replay requires your code to be deterministic – the same sequence of operations must occur in the same order on every invocation.

What Breaks Determinism

// ❌ Random control flow
if (Math.random() > 0.5) {
  await ctx.step('optional-step', async () => doSomething());
}

// ❌ Time‑based branching
const isWeekend = new Date().getDay() >= 5;
if (isWeekend) {
  await ctx.step('weekend-task', async () => doWeekendWork());
}

// ❌ External mutable state
let counter = 0;
await ctx.step('step1', async () => {
  counter++; // Won’t increment during replay!
  return counter;
});

These patterns cause mismatched operation sequences between runs, leading to replay consistency violations.

How to Write Deterministic Code

Capture all non‑deterministic values inside steps:

// ✅ Capture random values in steps
const randomId = await ctx.step('generate-id', async () => {
  return crypto.randomUUID(); // Executed once, cached on replay
});

// ✅ Capture timestamps in steps
const timestamp = await ctx.step('get-timestamp', async () => {
  return Date.now(); // Same timestamp on every replay
});

// ✅ Use event data for control flow (deterministic)
if (event.shouldProcess) {
  await ctx.step('process', async () => doWork());
}

// ✅ Capture time‑based decisions in steps
const isWeekend = await ctx.step('check-day', async () => {
  return new Date().getDay() >= 5;
});
if (isWeekend) {
  await ctx.step('weekend-task', async () => doWeekendWork());
}

Replay Consistency Validation

The SDK validates that each invocation follows the same operation sequence:

  • Operation type (STEP, WAIT, INVOKE)
  • Operation name (your identifier)
  • Operation position (sequential order)

Example validation error:

Replay consistency violation: Expected operation 'process-payment' 
of type STEP at position 2, but found operation 'send-email' of type STEP

This early detection helps you spot nondeterministic bugs.

A Complete Example: Order Processing with Replay

async function processOrder(event: any, ctx: DurableContext) {
  ctx.logger.info('Order processing started', { orderId: event.orderId });

  // Step 1: Validate inventory
  const inventory = await ctx.step('check-inventory', async () => {
    ctx.logger.info('Checking inventory');
    const response = await fetch(`https://api.inventory.com/check`, {
      method: 'POST',
      body: JSON.stringify({ items: event.items })
    });
    return response.json();
  });

  if (!inventory.available) {
    ctx.logger.warn('Out of stock', { missing: inventory.missing });
    return { status: 'out-of-stock' };
  }

  // Step 2: Process payment
  const payment = await ctx.step('process-payment', async () => {
    ctx.logger.info('Processing payment', { amount: inventory.total });
    const response = await fetch(`https://api.payments.com/charge`, {
      method: 'POST',
      body: JSON.stringify({
        customerId: event.customerId,
        amount: inventory.total
      })
    });
    return response.json();
  });

  // Step 3: Wait for warehouse confirmation (5‑minute timeout)
  ctx.logger.info('Waiting for warehouse confirmation');
  const confirmation = await ctx.waitForCallback(
    'warehouse-confirm',
    async (callbackId) => {
      // Send callback ID to warehouse system
      await fetch(`https://api.warehouse.com/notify`, {
        method: 'POST',
        body: JSON.stringify({ orderId: event.orderId, callbackId })
      });
    },
    { timeout: { seconds: 300 } }
  );

  // Step 4: Send notification
  const notification = await ctx.step('send-notification', async () => {
    ctx.logger.info('Sending notification');
    // e.g., call an email service
    await fetch(`https://api.email.com/send`, {
      method: 'POST',
      body: JSON.stringify({
        to: event.customerEmail,
        subject: 'Your order is confirmed',
        body: `Order ${event.orderId} is confirmed.`
      })
    });
    return { sent: true };
  });

  return { inventory, payment, confirmation, notification };
}

In this workflow, each step is checkpointed. If the function pauses (e.g., waiting for the warehouse callback), subsequent invocations replay the earlier steps instantly, guaranteeing exactly‑once execution of side‑effects.

Back to Blog

Related posts

Read more »