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:
- wait for a fixed duration.
- waitForCallback for an external system signalling completion.
- waitForCondition for polling a check function on a schedule.
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.
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.
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.
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.