Lambda Durable Patterns: Avoid Step Functions Until Complexity Earns It

Lambda Durable Patterns: Avoid Step Functions Until Complexity Earns It

Step Functions is excellent — and expensive in cognitive load — when your workflow is still “invoke, wait on webhook, continue.” Start with durable in-app patterns (token store + callback Lambda + idempotent resume) and graduate to Step Functions only when branching, human gates, or multi-service compensation justify the state machine.

⚡ TL;DR: Prefer DynamoDB task tokens + SQS/EventBridge callbacks for linear wait-and-resume; keep Lambda handlers idempotent with Powertools; move to Step Functions when you need visual branching, parallel fan-out with join, or audit-grade history. Pair with Lambda Timeouts, Retries, and DLQs and LLM Coding Agents on AWS.

The complexity tax

Signal Stay in-app Step Functions earns it
Steps 2–4 linear 5+ with branches
Wait One external callback Multiple waits + timers
Compensations Single undo Saga across 3+ services
Auditors App logs enough Need execution history UI
Team One squad owns flow Platform + many producers

❌ Wrapping every “sleep 30s and retry” in Express Workflows. ✅ Measuring whether operators actually open the SFN console during incidents.

In-app durable wait with a task token

// lib/durable.ts — illustrative callback pattern
import { DynamoDBClient, PutItemCommand, GetItemCommand } from "@aws-sdk/client-dynamodb";
import { randomUUID } from "node:crypto";

const ddb = new DynamoDBClient({});
const TABLE = process.env.TASK_TABLE!;

export async function startWait(input: { orderId: string }) {
  const taskToken = randomUUID();
  await ddb.send(new PutItemCommand({
    TableName: TABLE,
    Item: {
      pk: { S: `task#${taskToken}` },
      orderId: { S: input.orderId },
      status: { S: "WAITING" },
      ttl: { N: String(Math.floor(Date.now() / 1000) + 86400) },
    },
    ConditionExpression: "attribute_not_exists(pk)",
  }));
  // call external system with taskToken as correlation id
  return { taskToken };
}

export async function resume(taskToken: string, result: unknown) {
  const got = await ddb.send(new GetItemCommand({
    TableName: TABLE,
    Key: { pk: { S: `task#${taskToken}` } },
  }));
  if (!got.Item || got.Item.status.S !== "WAITING") {
    // ✅ Idempotent: duplicate callbacks are no-ops
    return { ok: false, reason: "not_waiting" };
  }
  // continue business logic…
  return { ok: true, result };
}

When to flip to Step Functions

// Only after complexity earns it — sketch
import * as sfn from "aws-cdk-lib/aws-stepfunctions";
import * as tasks from "aws-cdk-lib/aws-stepfunctions-tasks";

const waitForVendor = new tasks.LambdaInvoke(this, "WaitVendor", {
  lambdaFunction: vendorFn,
  integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
  payload: sfn.TaskInput.fromObject({
    token: sfn.JsonPath.taskToken,
    orderId: sfn.JsonPath.stringAt("$.orderId"),
  }),
});

const definition = submit
  .next(waitForVendor)
  .next(new sfn.Choice(this, "VendorOk?")
    .when(sfn.Condition.stringEquals("$.status", "OK"), fulfill)
    .otherwise(compensate));

Add SFN when Choice/Parallel/Map + execution replay beat bespoke Dynamo state. Until then, the in-app token table is enough — see also Lambda Warm Pools for keeping resume handlers warm.

Guardrails either way

  • Idempotency keys on every resume path
  • Explicit TTLs so abandoned waits die
  • Alarm on WAITING age p99
  • Never block a Lambda for wall-clock waits — always callback or SFN .wait

Closing checklist

✅ Dos
– ✅ Start with token store + callback for linear flows
– ✅ Idempotent resume; TTL abandoned tasks
– ✅ Promote to SFN when branching/saga/audit demand it
– ✅ Measure console usage before mandating SFN
– ✅ Keep payloads small; park blobs in S3

❌ Don’ts
– ❌ Don’t sleep inside Lambda for long waits
– ❌ Don’t invent a second orchestration framework
– ❌ Don’t skip compensation design “until later”
– ❌ Don’t put secrets in task-token payload

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply