Skip to content

Pause and resume

Durable functions can pause for seconds, hours, or days without keeping a Lambda invocation running. The SDK suspends execution, checkpoints the wait, and resumes the handler when the wait condition is met. This turns traditional polling loops and long-timeout wait-for-reply code into structured, cost-free pauses.

The durable wait operations cover delayed-work scenarios:

Prefer wait over sleep

Do not call language-specific methods like setTimeout, time.sleep, or Thread.sleep to pause a durable function. Those keep the invocation running and reset to zero on replay. The durable waits suspend the execution and do not incur compute cost while suspended.

// Wait 24 hours with no active Lambda time.
await context.wait("cool-off", { hours: 24 });
from aws_durable_execution_sdk_python.config import Duration

context.wait(Duration.from_hours(24), name="cool-off")
import java.time.Duration;

context.wait("cool-off", Duration.ofHours(24));
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;

public class WaitVsSleepExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<CoolOffEvent, string>(Workflow, input, context);

    private async Task<string> Workflow(CoolOffEvent input, IDurableContext ctx)
    {
        // Do NOT use Thread.Sleep or Task.Delay to pause a durable function.
        // They keep the invocation running (billing compute) and reset to zero
        // on replay:
        //     await Task.Delay(TimeSpan.FromHours(24)); // wrong

        // Use WaitAsync instead: the SDK suspends the execution, checkpoints the
        // wait, and re-invokes the handler when it elapses. No compute charge
        // while suspended. Name every wait so it reads clearly in logs and tests.
        await ctx.WaitAsync(TimeSpan.FromHours(24), name: "cool-off");

        ctx.Logger.LogInformation("Cool-off complete for {OrderId}", input.OrderId);
        return "done";
    }
}

public record CoolOffEvent(string OrderId);

Tip

Name every wait. Named waits show up in the operation history and CloudWatch, which keeps timelines legible in logs and test assertions.

Always set a callback timeout

waitForCallback suspends the execution until an external system calls the SDK's callback success or failure endpoints. Without a timeout the execution waits up to the execution timeout, holding the resource slot until an operator intervenes.

const outcome = await context.waitForCallback(
  "wait-for-approval",
  async (callbackId) => {
    await approvalsService.request({ id: event.orderId, callbackId });
  },
  { timeout: { hours: 24 } },
);
from aws_durable_execution_sdk_python.config import (
    Duration,
    WaitForCallbackConfig,
)

result = context.wait_for_callback(
    lambda callback_id, ctx: approvals_service.request(
        id=event["orderId"], callback_id=callback_id
    ),
    name="wait-for-approval",
    config=WaitForCallbackConfig(timeout=Duration.from_hours(24)),
)
import software.amazon.lambda.durable.config.CallbackConfig;
import software.amazon.lambda.durable.config.WaitForCallbackConfig;

WaitForCallbackConfig config = WaitForCallbackConfig.builder()
    .callbackConfig(CallbackConfig.builder()
        .timeout(Duration.ofHours(24))
        .build())
    .build();

Approval outcome = context.waitForCallback(
    "wait-for-approval",
    Approval.class,
    (callbackId, ctx) -> approvalsService.request(input.orderId(), callbackId),
    config);
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class CallbackTimeoutExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<OrderEvent, Approval>(Workflow, input, context);

    private async Task<Approval> Workflow(OrderEvent input, IDurableContext ctx)
    {
        // Always set a timeout on a callback. Without one the execution waits up
        // to the execution timeout, holding the resource slot until an operator
        // intervenes.
        var config = new WaitForCallbackConfig
        {
            Timeout = TimeSpan.FromHours(24),
        };

        // The submitter hands the service-allocated callbackId to the external
        // system. The external system later calls the SDK's callback success or
        // failure endpoint with that id.
        Approval outcome = await ctx.WaitForCallbackAsync<Approval>(
            async (callbackId, _, ct) =>
                await ApprovalsService.RequestAsync(input.OrderId, callbackId, ct),
            name: "wait-for-approval",
            config: config);

        return outcome;
    }
}

public record OrderEvent(string OrderId);
public record Approval(bool Approved);

public static class ApprovalsService
{
    public static Task RequestAsync(string orderId, string callbackId, CancellationToken ct)
        => Task.CompletedTask;
}

Danger

Always set a timeout on waitForCallback to avoid stalled executions.

When the timeout elapses, the SDK raises a callback timeout error. Either let it propagate to mark the step and the execution as failed, or catch it and handle it with compensatory actions.

Use heartbeats for long external operations

A heartbeat timeout fails the callback if the external system stops checking in, even if the overall timeout has not elapsed.

The external system has to call the SDK's heartbeat endpoint periodically while the work is in progress.

const outcome = await context.waitForCallback(
  "long-running-job",
  async (callbackId) => startJob({ jobId: event.jobId, callbackId }),
  {
    timeout: { hours: 24 },
    heartbeatTimeout: { minutes: 10 },
  },
);
config = WaitForCallbackConfig(
    timeout=Duration.from_hours(24),
    heartbeat_timeout=Duration.from_minutes(10),
)
CallbackConfig cb = CallbackConfig.builder()
    .timeout(Duration.ofHours(24))
    .heartbeatTimeout(Duration.ofMinutes(10))
    .build();
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class HeartbeatTimeoutExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<JobEvent, JobResult>(Workflow, input, context);

    private async Task<JobResult> Workflow(JobEvent input, IDurableContext ctx)
    {
        // HeartbeatTimeout fails the callback if the external worker stops
        // checking in, even before the overall Timeout elapses. Set it
        // comfortably longer than the expected interval between heartbeats but
        // shorter than the overall operation timeout.
        var config = new WaitForCallbackConfig
        {
            Timeout = TimeSpan.FromHours(24),
            HeartbeatTimeout = TimeSpan.FromMinutes(10),
        };

        // The external system must call the SDK's heartbeat endpoint periodically
        // while the work is in progress, and the success/failure endpoint when done.
        JobResult outcome = await ctx.WaitForCallbackAsync<JobResult>(
            async (callbackId, _, ct) =>
                await JobService.StartAsync(input.JobId, callbackId, ct),
            name: "long-running-job",
            config: config);

        return outcome;
    }
}

public record JobEvent(string JobId);
public record JobResult(string Status);

public static class JobService
{
    public static Task StartAsync(string jobId, string callbackId, CancellationToken ct)
        => Task.CompletedTask;
}

Warning

A 24-hour timeout means a 24-hour outage when the external worker crashes. Set a heartbeat timeout comfortably longer than the expected interval between heartbeats, but shorter than the overall operation timeout.

Poll external services with waitForCondition

Use waitForCondition for external systems where you have to poll rather than create a callback.

The SDK runs your check function, applies a wait strategy between polls, and resumes when the check signals completion. Each poll is a step. The wait between polls suspends the execution.

Use an exponential backoff wait strategy so an unresponsive downstream system does not create a retry storm.

const final = await context.waitForCondition(
  "wait-for-job",
  async (state, ctx) => {
    const status = await jobService.getStatus(state.jobId);
    return { ...state, status };
  },
  {
    initialState: { jobId: event.jobId, status: "pending" },
    waitStrategy: (state, attempt) => {
      if (state.status === "completed") return { shouldContinue: false };
      const delaySeconds = Math.min(2 ** attempt, 60);
      return { shouldContinue: true, delay: { seconds: delaySeconds } };
    },
  },
);
from aws_durable_execution_sdk_python.config import Duration
from aws_durable_execution_sdk_python.waits import WaitForConditionConfig


def check(state, ctx):
    state["status"] = job_service.get_status(state["jobId"])
    return state


def wait_strategy(state, attempt):
    if state["status"] == "completed":
        return {"should_continue": False}
    delay = min(2 ** attempt, 60)
    return {"should_continue": True, "delay": Duration.from_seconds(delay)}


final_state = context.wait_for_condition(
    check,
    WaitForConditionConfig(
        initial_state={"jobId": event["jobId"], "status": "pending"},
        wait_strategy=wait_strategy,
    ),
    name="wait-for-job",
)
import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.config.WaitForConditionConfig;
import software.amazon.lambda.durable.model.WaitForConditionResult;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.WaitStrategies;

var strategy = WaitStrategies.<Map<String, Object>>exponentialBackoff(
    60, Duration.ofSeconds(5), Duration.ofMinutes(1), 2.0, JitterStrategy.FULL);

var config = WaitForConditionConfig.<Map<String, Object>>builder()
    .initialState(Map.of("jobId", input.jobId(), "status", "pending"))
    .waitStrategy(strategy)
    .build();

Map<String, Object> finalState = context.waitForCondition(
    "wait-for-job",
    new TypeToken<>() {},
    (state, stepCtx) -> {
        String status = jobService.getStatus((String) state.get("jobId"));
        var updated = Map.<String, Object>of("jobId", state.get("jobId"), "status", status);
        return "completed".equals(status)
            ? WaitForConditionResult.stopPolling(updated)
            : WaitForConditionResult.continuePolling(updated);
    },
    config);
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

public class WaitForConditionExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<JobEvent, JobState>(Workflow, input, context);

    private async Task<JobState> Workflow(JobEvent input, IDurableContext ctx)
    {
        // Use an exponential-backoff wait strategy so an unresponsive downstream
        // system does not create a retry storm. The isDone predicate stops
        // polling once the state satisfies the condition.
        var strategy = WaitStrategy.Exponential<JobState>(
            maxAttempts: 60,
            initialDelay: TimeSpan.FromSeconds(5),
            maxDelay: TimeSpan.FromMinutes(1),
            backoffRate: 2.0,
            jitter: JitterStrategy.Full,
            isDone: state => state.Status == "completed");

        var config = new WaitForConditionConfig<JobState>
        {
            InitialState = new JobState(input.JobId, "pending"),
            WaitStrategy = strategy,
        };

        // The SDK runs the check on each poll, applies the wait strategy between
        // polls (suspending the execution — no compute charge), and resumes when
        // the strategy stops. Each poll is a step.
        JobState finalState = await ctx.WaitForConditionAsync<JobState>(
            async (state, _, ct) =>
            {
                string status = await JobService.GetStatusAsync(state.JobId, ct);
                return state with { Status = status };
            },
            config,
            name: "wait-for-job");

        return finalState;
    }
}

public record JobEvent(string JobId);
public record JobState(string JobId, string Status);

public static class JobService
{
    public static Task<string> GetStatusAsync(string jobId, CancellationToken ct)
        => Task.FromResult("completed");
}

Tip

Durable waits round up to a minimum of one second. Don't use wait for condition to poll for sub-second low-latency state changes.

See also