惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
B
Blog RSS Feed
Y
Y Combinator Blog
T
Tailwind CSS Blog
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
aimingoo的专栏
aimingoo的专栏
Jina AI
Jina AI
The GitHub Blog
The GitHub Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
A
About on SuperTechFans
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Docker
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Check Point Blog
M
MIT News - Artificial intelligence
Last Week in AI
Last Week in AI
V
V2EX
腾讯CDC
F
Fortinet All Blogs
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Lambda Durable Functions, When You Don't Need Step Functions
Lewis Sawe · 2026-05-13 · via DEV Community

At re:Invent 2025, AWS added a new primitive to Lambda called durable execution. Your function can now checkpoint its progress, survive failures without restarting from scratch, and suspend for up to a year without paying for compute time.

If you've used Azure Durable Functions or Temporal, the concept is familiar. If you haven't, the short version is that your Lambda function can pause mid-execution, wait for something external (a human approval, a webhook, a scheduled delay), and resume exactly where it left off. No DynamoDB state table. No Step Functions state machine. Just code.

This post covers what it is, how it works, when to use it instead of Step Functions, and what it actually looks like to build with.

The Problem It Solves

Standard Lambda functions run from start to finish in a single invocation. Maximum 15 minutes. If something fails at step 7 of 10, you retry the whole thing. If you need to wait for a human to click "approve," you need to save state somewhere, set up an API Gateway endpoint to receive the callback, wire up the resume logic, and handle the case where your function code changed between the pause and the resume.

Most teams solve this with Step Functions, which works well but means defining your workflow in Amazon States Language (a JSON DSL) or the CDK Step Functions constructs. Your business logic lives in Lambda, your orchestration logic lives in Step Functions, and you context-switch between two mental models.

Lambda Durable Functions puts both in the same file.

How It Works

You enable durable execution when you create the function (a DurableConfig block in your SAM template or CDK construct). Then you use the durable execution SDK in your handler code.

The SDK gives you a few primitives.

  • context.step() runs a block of code and checkpoints the result. If the function fails later and replays, this step is skipped and its cached result is returned.
  • context.wait() suspends execution for a duration (minutes, hours, days). No compute charges during the wait.
  • context.waitForCallback() suspends until an external system calls back with a success or failure. Up to one year.
  • context.waitForCondition() polls a condition on a schedule until it returns true.
  • context.parallel() runs multiple durable operations concurrently.
  • context.invoke() calls another Lambda function and checkpoints the result.

Under the hood, Lambda uses a checkpoint/replay mechanism. When your function resumes (after a wait, a failure, or a deployment), Lambda invokes your handler from the top. The SDK replays through completed steps instantly (returning cached results) and picks up execution at the point where it left off.

checkpoint/replay diagram

What It Looks Like

A user onboarding flow that creates a profile, waits up to 24 hours for email verification, then sends a welcome email.

import { DurableContext, withDurableExecution } from '@aws/durable-execution-sdk-js';

export const handler = withDurableExecution(
  async (event, context: DurableContext) => {
    const profile = await context.step("create-profile", async () =>
      createUserProfile(event.email, event.name)
    );

    const verification = await context.waitForCallback(
      "wait-for-email-verification",
      async (callbackId) => {
        await sendVerificationEmail(profile, callbackId);
      },
      { timeout: { hours: 24 } }
    );

    const result = await context.step("complete-onboarding", async () => {
      if (!verification || !verification.verified) {
        return { ...profile, status: 'failed' };
      }
      await sendWelcomeEmail(profile.email, profile.name);
      return { ...profile, status: 'active' };
    });

    return result;
  }
);

Enter fullscreen mode Exit fullscreen mode

If createUserProfile succeeds but the verification email fails to send, the function retries from the waitForCallback step. The profile creation is skipped (already checkpointed). If the user clicks verify 6 hours later, the function resumes at the complete-onboarding step. During those 6 hours, you pay nothing.

The SAM template looks like this.

Resources:
  UserOnboardingFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: UserOnboardingFunction
      CodeUri: ./src
      Handler: index.handler
      Runtime: nodejs24.x
      Timeout: 60
      DurableConfig:
        ExecutionTimeout: 90000  # 25 hours
        RetentionPeriodInDays: 7

Enter fullscreen mode Exit fullscreen mode

Timeout is still the per-invocation limit (max 15 minutes). ExecutionTimeout is how long the entire durable execution can run, including waits. Up to one year.

When to Use This Instead of Step Functions

The AWS docs frame it as "application development in Lambda" vs. "workflow orchestration across AWS services." Here's my less diplomatic version.

side-by-side comparison, Step Functions state machine vs. durable function code file

Use Durable Functions when

  • Your team prefers standard programming languages and familiar development tools
  • Your workflow is mostly Lambda code calling other services via SDK
  • You want the orchestration logic in the same file as the business logic
  • You're comfortable with the checkpoint/replay model
  • You want to test locally with sam local invoke

Use Step Functions when

  • You're orchestrating many AWS services (SQS, SNS, DynamoDB, ECS) with native integrations
  • Non-engineers need to understand the workflow (the visual designer matters)
  • You want zero runtime maintenance (no SDK versions to update)
  • You need the 220+ direct service integrations without writing Lambda functions for each

Use both when

  • Step Functions coordinates the high-level flow across services
  • Durable Functions handles complex application logic within individual Lambda functions

The honest comparison is that Durable Functions is simpler for Lambda-heavy workflows. Step Functions is more powerful for cross-service orchestration. If your workflow is "Lambda calls Lambda calls Lambda with some waits in between," Durable Functions is less overhead. If your workflow is "receive SQS message, write to DynamoDB, start ECS task, wait for completion, send SNS notification," Step Functions has native integrations for all of that without writing Lambda handlers.

What You Pay For

Standard Lambda compute charges apply to all invocations, including replays. During waits, on-demand functions don't incur duration charges.

You also pay for durable operations (each step(), wait(), invoke() call), data written to checkpoints, and data retention (how long execution history is kept).

For workflows that spend most of their time waiting (approval flows, scheduled delays, polling), the cost model is favorable. You're not paying for an idle Step Functions execution either, but the per-operation pricing differs. For short, compute-heavy workflows with few waits, the replay overhead adds cost that Step Functions doesn't have.

cost timeline showing active compute (colored) vs. suspended wait time (empty)

Things to Know Before You Build

Determinism matters. Your handler runs from the top on every replay. Code between steps must be deterministic. Don't generate random IDs outside a step, don't read the current time outside a step, don't make API calls outside a step. Anything with side effects goes inside context.step().

Versioning is strict. Use function versions or aliases, not $LATEST. If your code changes between a pause and a resume, the replay might not match the original execution. Lambda replays with the version that started the execution.

15-minute invocation limit still applies. Each individual invocation (including replays) must complete within 15 minutes. The durable execution can span months, but each "wake up" is still a normal Lambda invocation. If your replay takes too long because you have hundreds of completed steps to skip through, that's a problem.

You can't add DurableConfig to an existing function. You need to create a new function with durable execution enabled from the start.

SDKs available for Node.js, Python, and Java. Java SDK went GA in April 2026. Python and Node.js have been available since launch.

Available in 16+ regions as of April 2026.

A Practical Example, Order Processing

sequence diagram showing order flow with suspension period marked

Here's a more realistic example. An order comes in, you validate payment, reserve inventory, wait for the warehouse to confirm shipment (could take hours), then notify the customer.

from aws_durable_execution_sdk import durable_function, DurableContext

@durable_function
def handler(event, context: DurableContext):
    order = event

    # Validate payment
    payment = context.step("validate-payment", lambda: charge_card(order))

    if not payment["success"]:
        return {"status": "payment_failed", "order_id": order["id"]}

    # Reserve inventory
    reservation = context.step("reserve-inventory", lambda: reserve_items(order["items"]))

    # Wait for warehouse confirmation (poll every 5 minutes, timeout after 48 hours)
    shipment = context.wait_for_condition(
        "wait-for-shipment",
        check=lambda: check_shipment_status(reservation["id"]),
        interval={"minutes": 5},
        timeout={"hours": 48}
    )

    # Notify customer
    context.step("notify-customer", lambda: send_shipping_notification(
        order["email"], shipment["tracking_number"]
    ))

    return {"status": "shipped", "tracking": shipment["tracking_number"]}

Enter fullscreen mode Exit fullscreen mode

If the warehouse takes 6 hours to ship, the function wakes up every 5 minutes, checks the status, and goes back to sleep. You pay for ~72 invocations of a few hundred milliseconds each, not 6 hours of compute.

Compare this to the Step Functions version. You'd need a state machine with a Wait state, a Choice state, a Lambda function for each step, and the wiring between them. It works, but it's more infrastructure for the same outcome.

Who This Is For

The pattern that makes durable functions worth it is any workflow where you have seconds of actual work separated by hours or days of waiting for something external. The more wait time relative to compute time, the better the cost model.

A few examples that aren't the usual "order processing" and "user onboarding" from the AWS docs.

Trial expiration flow. User signs up for a 14-day trial. The function suspends for 14 days, wakes up, checks if they converted to paid, sends the appropriate email (upgrade nudge or offboarding notice). One function, one execution, two weeks of zero cost. No cron job, no scheduler, no DynamoDB TTL workaround.

export const handler = withDurableExecution(
  async (event, context) => {
    const user = await context.step("create-trial", async () =>
      createTrialAccount(event.email, event.plan)
    );

    await context.wait("trial-period", { days: 14 });

    const converted = await context.step("check-conversion", async () =>
      hasUpgradedToPaid(user.id)
    );

    await context.step("send-email", async () => {
      if (converted) return sendRetentionEmail(user.email);
      return sendOffboardingEmail(user.email);
    });

    if (!converted) {
      await context.step("deactivate", async () => deactivateAccount(user.id));
    }

    return { userId: user.id, converted };
  }
);

Enter fullscreen mode Exit fullscreen mode

Webhook delivery with backoff. First attempt fails. Wait 1 minute, retry. Wait 5 minutes, retry. Wait 30 minutes, retry. The waits cost nothing.

export const handler = withDurableExecution(
  async (event, context) => {
    const { url, payload, maxAttempts = 4 } = event;
    const delays = [0, 1, 5, 30]; // minutes

    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      if (delays[attempt] > 0) {
        await context.wait(`backoff-${attempt}`, { minutes: delays[attempt] });
      }

      const result = await context.step(`deliver-${attempt}`, async () =>
        deliverWebhook(url, payload)
      );

      if (result.success) return { delivered: true, attempts: attempt + 1 };
    }

    await context.step("mark-failed", async () =>
      markWebhookFailed(event.webhookId)
    );

    return { delivered: false, attempts: maxAttempts };
  }
);

Enter fullscreen mode Exit fullscreen mode

Infrastructure provisioning with approval gates. Terraform plan runs, posts to Slack, suspends until someone approves.

export const handler = withDurableExecution(
  async (event, context) => {
    const plan = await context.step("terraform-plan", async () =>
      runTerraformPlan(event.workspace)
    );

    if (plan.changes === 0) return { status: "no_changes" };

    const approval = await context.waitForCallback(
      "wait-for-approval",
      async (callbackId) => {
        await postToSlack(event.channel, {
          text: `Terraform wants to change ${plan.changes} resources in ${event.workspace}`,
          planUrl: plan.outputUrl,
          approveUrl: buildApproveUrl(callbackId),
          rejectUrl: buildRejectUrl(callbackId)
        });
      },
      { timeout: { hours: 24 } }
    );

    if (!approval || !approval.approved) {
      return { status: "rejected", by: approval?.user };
    }

    const apply = await context.step("terraform-apply", async () =>
      runTerraformApply(event.workspace)
    );

    return { status: "applied", resources: apply.changed };
  }
);

Enter fullscreen mode Exit fullscreen mode

Incident response with human escalation. Auto-isolate, alert, wait for analyst confirmation.

export const handler = withDurableExecution(
  async (event, context) => {
    const finding = event.detail;

    const isolation = await context.step("isolate-instance", async () =>
      isolateEc2Instance(finding.instanceId)
    );

    const decision = await context.waitForCallback(
      "wait-for-analyst",
      async (callbackId) => {
        await createPagerDutyIncident({
          title: `Isolated ${finding.instanceId} - ${finding.type}`,
          details: finding,
          callbackId
        });
      },
      { timeout: { hours: 4 } }
    );

    if (!decision || decision.action === "terminate") {
      await context.step("terminate", async () =>
        terminateInstance(finding.instanceId)
      );
      return { outcome: "terminated" };
    }

    await context.step("restore", async () =>
      restoreInstance(finding.instanceId, isolation.previousSecurityGroups)
    );

    return { outcome: "restored" };
  }
);

Enter fullscreen mode Exit fullscreen mode

Scheduled certificate rotation. Check, renew, wait for DNS validation, swap.

export const handler = withDurableExecution(
  async (event, context) => {
    const cert = await context.step("check-expiry", async () =>
      getCertificateDetails(event.certArn)
    );

    if (cert.daysUntilExpiry > 30) return { status: "not_due" };

    const renewal = await context.step("request-renewal", async () =>
      requestCertificate(cert.domain)
    );

    const validated = await context.waitForCondition(
      "wait-for-validation",
      async () => checkValidationStatus(renewal.arn) === "ISSUED",
      { interval: { minutes: 2 }, timeout: { minutes: 30 } }
    );

    if (!validated) return { status: "validation_failed" };

    await context.step("swap-cert", async () =>
      updateListenerCertificate(event.listenerArn, renewal.arn)
    );

    return { status: "rotated", newCertArn: renewal.arn };
  }
);

Enter fullscreen mode Exit fullscreen mode

The common thread is that these workflows are too simple for Step Functions (5-7 steps, mostly Lambda code) but too stateful for a single Lambda invocation. That's the gap durable functions fill.


Getting Started

sam init  # Choose the durable functions quick start template
sam local invoke  # Test locally, including callbacks
sam deploy  # Ship it

Enter fullscreen mode Exit fullscreen mode

The developer guide covers setup. The re:Invent breakout session is worth watching for the architecture deep dive.

Lambda Durable Functions | Documentation | Pricing