Skip to content

Wait Operations

Time-based durable waits

The Wait operation of the DurableContext pauses execution for a specified time without consuming compute. The SDK will checkpoint the start of the wait operation, the function suspends and Lambda exits, and then the backend automatically resumes execution when the wait completes. The SDK will replay and resume processing from where it had paused for the wait.

Unlike language-native sleep functions such as setTimeout(), time.sleep(), or Thread.sleep(), durable wait operations do not consume Lambda execution time. The durable function invocation exits cleanly after it checkpoints the start of the wait and resumes later at the specified time, even if the wait lasts hours or days.

The minimum wait duration is 1 second. The maximum wait duration is the maximum execution duration of 1 year. There is no cost associated with longer waits.

You cannot cancel a wait after it has started.

When to use wait

Use context.wait() for a time-based delay. For example, use waits between steps to delay the new next step in multi-step workflows.

Wait for an event or status change

To wait for an event or status change, rather than just a fixed delay, consider these alternatives:

  • Polling until a condition is met
  • Waiting for an external system response
    • Callbacks suspend your durable function until an external system sends a response.

Wait walkthrough

Here's a simple example of using a wait operation:

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

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Wait for 5 seconds
    await context.wait({ seconds: 5 });
    return "Wait completed";
  },
);
from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python.config import Duration

@durable_execution
def handler(event: dict, context: DurableContext) -> str:
    """Simple durable function with a wait."""
    # Wait for 5 seconds
    context.wait(duration=Duration.from_seconds(5))
    return "Wait completed"
import java.time.Duration;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class BasicWaitExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        // Wait for 5 seconds
        context.wait(null, Duration.ofSeconds(5));
        return "Wait completed";
    }
}
package main

import (
    "time"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

func handler(ctx durable.Context, event map[string]any) (string, error) {
    // Wait for 5 seconds. The duration is a standard library time.Duration.
    if err := durable.Wait(ctx, "", 5*time.Second); err != nil {
        return "", err
    }
    return "Wait completed", nil
}

func main() {
    durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

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

    private async Task<string> Workflow(object input, IDurableContext ctx)
    {
        // Wait for 5 seconds
        await ctx.WaitAsync(TimeSpan.FromSeconds(5));
        return "Wait completed";
    }
}

When this function runs:

  1. The SDK checkpoints the wait operation with a scheduled end time
  2. The Lambda function suspends
  3. After 5 seconds, the backend automatically invokes your function again
  4. Execution resumes after the wait and returns "Wait completed"

Method signature

wait(duration: Duration): DurablePromise<void>;
wait(name: string, duration: Duration): DurablePromise<void>;
def wait(
    self,
    duration: Duration,
    name: str | None = None,
) -> None
Void wait(String name, Duration duration);
DurableFuture<Void> waitAsync(String name, Duration duration);

Set name to null to omit it.

// Sync
func Wait(ctx Context, name string, d time.Duration) error

// Async
func WaitAsync(ctx Context, name string, d time.Duration) *Future[Void]

Pass "" to omit the name.

Task WaitAsync(
    TimeSpan duration,
    string? name = null,
    CancellationToken cancellationToken = default);

Omit name to infer one from the call site.

Parameters:

  • duration (required) - How long to wait. Must be at least 1 second. See Duration for how to specify durations in each programming language.
  • name (optional) - Only used for display, debugging and testing.

Returns:

DurablePromise<void>

None

Void (sync)

DurableFuture<Void> (async)

error (sync, nil on success)

*durable.Future[durable.Void] (async)

Task

Raises/Throws:

None

ValidationError(DurableExecutionsError)

IllegalArgumentException

Wait returns a plain error for a duration under one second, including zero and negative durations. It then records no operation and writes no checkpoint. WaitAsync returns a future that fails with the same error. The SDK rounds a duration of one second or more up to whole seconds. It does not check the one-year maximum.

ArgumentOutOfRangeException

Duration

Duration signature

type Duration =
  | { days: number; hours?: number; minutes?: number; seconds?: number }
  | { hours: number; minutes?: number; seconds?: number }
  | { minutes: number; seconds?: number }
  | { seconds: number };
# module: aws_durable_execution_sdk_python.config


@dataclass
class Duration:
    seconds: int = 0

    @classmethod
    def from_seconds(cls, value: float) -> Duration: ...

    @classmethod
    def from_minutes(cls, value: float) -> Duration: ...

    @classmethod
    def from_hours(cls, value: float) -> Duration: ...

    @classmethod
    def from_days(cls, value: float) -> Duration: ...
// java.time.Duration (standard library)
Duration.ofSeconds(long seconds);
Duration.ofMinutes(long minutes);
Duration.ofHours(long hours);
Duration.ofDays(long days);

The SDK uses the standard library time.Duration.

// time.Duration (standard library)
type Duration int64

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)
// System.TimeSpan (standard library)
TimeSpan.FromSeconds(double seconds);
TimeSpan.FromMinutes(double minutes);
TimeSpan.FromHours(double hours);
TimeSpan.FromDays(double days);

Duration usage

// Wait for 30 seconds
await context.wait({ seconds: 30 });

// Wait for 5 minutes
await context.wait({ minutes: 5 });

// Wait for 2 hours
await context.wait({ hours: 2 });

// Wait for 1 day
await context.wait({ days: 1 });

// Combined - 1 hour and 30 minutes
await context.wait({ hours: 1, minutes: 30 });
from aws_durable_execution_sdk_python.config import Duration

# Wait for 30 seconds
context.wait(duration=Duration.from_seconds(30))

# Wait for 5 minutes
context.wait(duration=Duration.from_minutes(5))

# Wait for 2 hours
context.wait(duration=Duration.from_hours(2))

# Wait for 1 day
context.wait(duration=Duration.from_days(1))
import java.time.Duration;

// Wait for 30 seconds
context.wait("wait30", Duration.ofSeconds(30));

// Wait for 5 minutes
context.wait("wait5m", Duration.ofMinutes(5));

// Wait for 2 hours
context.wait("wait2h", Duration.ofHours(2));

// Wait for 1 day
context.wait("wait1d", Duration.ofDays(1));
// Wait for 30 seconds
if err := durable.Wait(ctx, "wait30", 30*time.Second); err != nil {
    return err
}

// Wait for 5 minutes
if err := durable.Wait(ctx, "wait5m", 5*time.Minute); err != nil {
    return err
}

// Wait for 2 hours
if err := durable.Wait(ctx, "wait2h", 2*time.Hour); err != nil {
    return err
}

// Wait for 1 day. The time package has no day unit.
if err := durable.Wait(ctx, "wait1d", 24*time.Hour); err != nil {
    return err
}

// Combined: 1 hour and 30 minutes
if err := durable.Wait(ctx, "wait90m", time.Hour+30*time.Minute); err != nil {
    return err
}
// Wait for 30 seconds
await ctx.WaitAsync(TimeSpan.FromSeconds(30), name: "wait30");

// Wait for 5 minutes
await ctx.WaitAsync(TimeSpan.FromMinutes(5), name: "wait5m");

// Wait for 2 hours
await ctx.WaitAsync(TimeSpan.FromHours(2), name: "wait2h");

// Wait for 1 day
await ctx.WaitAsync(TimeSpan.FromDays(1), name: "wait1d");

Named wait operations

Name wait operations to make them easier to identify in logs and tests.

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

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Wait with explicit name
    await context.wait("custom_wait", { seconds: 2 });
    return "Wait with name completed";
  },
);
from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python.config import Duration

@durable_execution
def handler(event: dict, context: DurableContext) -> str:
    """Durable function with a named wait."""
    # Wait with explicit name
    context.wait(duration=Duration.from_seconds(2), name="custom_wait")
    return "Wait with name completed"
import java.time.Duration;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class NamedWaitExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        // Wait with explicit name
        context.wait("custom_wait", Duration.ofSeconds(2));
        return "Wait with name completed";
    }
}
package main

import (
    "time"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

func handler(ctx durable.Context, event map[string]any) (string, error) {
    // The name is the second argument, after the context.
    if err := durable.Wait(ctx, "custom_wait", 2*time.Second); err != nil {
        return "", err
    }
    return "Wait with name completed", nil
}

func main() {
    durable.Start(handler)
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

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

    private async Task<string> Workflow(object input, IDurableContext ctx)
    {
        // Wait with explicit name
        await ctx.WaitAsync(TimeSpan.FromSeconds(2), name: "custom_wait");
        return "Wait with name completed";
    }
}

Scheduled end timestamp

Each wait operation has a scheduled end timestamp that indicates when it completes. This timestamp uses Unix milliseconds.

The ScheduledEndTimestamp field is in the checkpoint's WaitDetails. The SDK calculates the scheduled end time when it first checkpoints the wait operation:

{current time} + {wait duration} = {scheduled end timestamp}

Wait durations are approximate. The actual resume time depends on system scheduling, Lambda cold start time, and current system load.

Concurrency

Waits execute sequentially in the order they appear in your code. You can use waits inside parallel or map operations. If a branch or iteration of a parent is ready to suspend due to a wait, the durable function will wait for all child operations of that parent to complete or suspend before terminating the invocation.

You can run a wait concurrently with other operations. This is useful for enforcing a minimum duration — for example, ensuring at least 5 seconds pass while a step runs in parallel.

Don't await the wait immediately — use Promise.all to run it alongside other operations.

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Start wait and step but don't await yet
    const waitPromise = context.wait("min-delay", { seconds: 5 });
    const stepPromise = context.step("process", async () => processData(event));

    // Wait for both — guarantees at least 5 seconds elapsed
    const [, result] = await Promise.all([waitPromise, stepPromise]);

    return result;
  },
);

Python waits are synchronous only. Use parallel or map for concurrency.

Use waitAsync() which returns a DurableFuture<Void>, then call .get() when you need to block.

import java.time.Duration;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableFuture;
import software.amazon.lambda.durable.DurableHandler;

public class AsyncWaitExample extends DurableHandler<Object, String> {
    @Override
    public String handleRequest(Object input, DurableContext context) {
        // Start wait and step concurrently — returns immediately
        DurableFuture<Void> waitFuture = context.waitAsync("min-delay", Duration.ofSeconds(5));
        DurableFuture<String> stepFuture = context.stepAsync("process", String.class, stepCtx -> processData(input));

        // Block until both complete — guarantees at least 5 seconds elapsed
        waitFuture.get();
        var result = stepFuture.get();

        return result;
    }
}

Await the futures with durable.Join, not with one Result call after another. A sequence of Result calls that returns on the first error leaves the other futures unawaited. Their progress is then not checkpointed when the invocation suspends. Join runs in a child context, so it records one more operation.

package main

import (
    "time"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

func process(event map[string]any) (string, error) {
    return "processed", nil
}

func handler(ctx durable.Context, event map[string]any) (string, error) {
    // Start the wait and the step without awaiting either yet.
    waitFut := durable.WaitAsync(ctx, "min-delay", 5*time.Second)
    stepFut := durable.StepAsync(ctx, "process", func(sc durable.StepContext) (string, error) {
        return process(event)
    })

    // Await both with a combinator so a suspension drains every branch.
    // This guarantees at least 5 seconds elapsed.
    if err := durable.Join(ctx, "min-delay-and-process", []durable.Awaitable{waitFut, stepFut}); err != nil {
        return "", err
    }
    return stepFut.Result(ctx)
}

func main() {
    durable.Start(handler)
}

Don't await the wait immediately. Capture the Task and use Task.WhenAll to run it alongside other operations.

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

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

    private async Task<string> Workflow(object input, IDurableContext ctx)
    {
        // Start the wait and the step but don't await yet — WaitAsync returns a Task
        Task waitTask = ctx.WaitAsync(TimeSpan.FromSeconds(5), name: "min-delay");
        Task<string> stepTask = ctx.StepAsync(
            async (_, _) => ProcessData(input),
            name: "process");

        // Await both — guarantees at least 5 seconds elapsed
        await Task.WhenAll(waitTask, stepTask);

        return await stepTask;
    }

    private static string ProcessData(object input) => "processed";
}

Testing

You can verify wait operations in your tests by inspecting the operations list:

import {
  LocalDurableTestRunner,
  OperationType,
  OperationStatus,
} from "@aws/durable-execution-sdk-js-testing";
import { handler } from "./multiple-waits";

let runner: LocalDurableTestRunner;

beforeAll(async () => {
  await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true });
});

afterAll(async () => {
  await LocalDurableTestRunner.teardownTestEnvironment();
});

beforeEach(() => {
  runner = new LocalDurableTestRunner({ handlerFunction: handler });
});

it("should handle multiple sequential wait operations", async () => {
  const firstWait = runner.getOperation("wait-1");
  const secondWait = runner.getOperation("wait-2");

  const result = await runner.run();

  expect(result.getResult()).toEqual({ completedWaits: 2, finalStep: "done" });

  const operations = result.getOperations();
  expect(operations.length).toEqual(2);

  expect(firstWait.getType()).toBe(OperationType.WAIT);
  expect(firstWait.getStatus()).toBe(OperationStatus.SUCCEEDED);
  expect(firstWait.getWaitDetails()?.waitSeconds).toBe(5);

  expect(secondWait.getType()).toBe(OperationType.WAIT);
  expect(secondWait.getStatus()).toBe(OperationStatus.SUCCEEDED);
  expect(secondWait.getWaitDetails()?.waitSeconds).toBe(5);
});
@pytest.mark.durable_execution(handler=multiple_wait.handler)
def test_multiple_waits(durable_runner):
    """Test multiple sequential waits."""
    with durable_runner:
        result = durable_runner.run(input="test", timeout=20)

    assert result.status is InvocationStatus.SUCCEEDED

    # Find wait operations by name
    wait_1 = result.get_wait("wait-1")
    wait_2 = result.get_wait("wait-2")

    assert wait_1.scheduled_end_timestamp is not None
    assert wait_2.scheduled_end_timestamp is not None
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import software.amazon.lambda.durable.model.ExecutionStatus;
import software.amazon.awssdk.services.lambda.model.OperationStatus;
import software.amazon.awssdk.services.lambda.model.OperationType;
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;

@Test
void testMultipleWaits() {
    var runner = LocalDurableTestRunner.create(Object.class, new MultipleWaitExample());

    var result = runner.runUntilComplete("test");

    assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());

    // Find all wait operations
    var waitOps = result.getOperations().stream()
        .filter(op -> op.getType() == OperationType.WAIT)
        .toList();
    assertEquals(2, waitOps.size());

    // Verify both waits have names
    var waitNames = waitOps.stream().map(op -> op.getName()).toList();
    assertTrue(waitNames.contains("wait-1"));
    assertTrue(waitNames.contains("wait-2"));
}

Save this as wait_test.go. It uses the durabletest in-memory runner.

package main

import (
    "testing"
    "time"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
    "github.com/aws/aws-durable-execution-sdk-go/durable/durabletest"
)

type waitResult struct {
    CompletedWaits int    `json:"completedWaits"`
    FinalStep      string `json:"finalStep"`
}

func multipleWaitsHandler(ctx durable.Context, event map[string]any) (waitResult, error) {
    if err := durable.Wait(ctx, "wait-1", 5*time.Second); err != nil {
        return waitResult{}, err
    }
    if err := durable.Wait(ctx, "wait-2", 5*time.Second); err != nil {
        return waitResult{}, err
    }
    return waitResult{CompletedWaits: 2, FinalStep: "done"}, nil
}

func TestMultipleWaits(t *testing.T) {
    runner := durabletest.NewLocalRunner(multipleWaitsHandler)

    result, err := runner.RunUntilComplete(map[string]any{})
    if err != nil {
        t.Fatal(err)
    }
    if result.Status != durabletest.Succeeded {
        t.Fatalf("status = %s, want SUCCEEDED", result.Status)
    }

    out, err := durabletest.ResultAs[waitResult](result)
    if err != nil {
        t.Fatal(err)
    }
    if out.CompletedWaits != 2 || out.FinalStep != "done" {
        t.Fatalf("result = %+v", out)
    }

    if len(result.Operations) != 2 {
        t.Fatalf("got %d operations, want 2:\n%s", len(result.Operations), result.FormatTree())
    }

    for _, name := range []string{"wait-1", "wait-2"} {
        op := result.Operation(name)
        if op == nil {
            t.Fatalf("missing wait %q:\n%s", name, result.FormatTree())
        }
        if op.Type != "WAIT" || op.Status != "SUCCEEDED" {
            t.Fatalf("%s: type=%s status=%s", name, op.Type, op.Status)
        }
        if op.WaitDetails == nil || op.WaitDetails.WaitSeconds != 5 {
            t.Fatalf("%s: unexpected wait details %+v", name, op.WaitDetails)
        }
    }
}
using System.Linq;
using Amazon.Lambda.DurableExecution;
using Amazon.Lambda.DurableExecution.Testing;
using Xunit;

public class MultipleWaitsTests
{
    [Fact]
    public async Task HandlesMultipleSequentialWaits()
    {
        // SkipTime fast-forwards waits so the test runs instantly.
        await using var runner = new DurableTestRunner<object, MultipleWaitsResult>(
            handler: Workflow,
            options: new TestRunnerOptions { SkipTime = true });

        var result = await runner.RunAsync("test");
        result.EnsureSucceeded();

        Assert.Equal(2, result.Result!.CompletedWaits);
        Assert.Equal("done", result.Result!.FinalStep);

        // Both waits are recorded with their names.
        var waitOps = result.Steps.Where(s => s.Kind == OperationKind.Wait).ToList();
        Assert.Equal(2, waitOps.Count);

        var waitNames = waitOps.Select(w => w.Name).ToList();
        Assert.Contains("wait-1", waitNames);
        Assert.Contains("wait-2", waitNames);
    }

    private static async Task<MultipleWaitsResult> Workflow(object input, IDurableContext ctx)
    {
        await ctx.WaitAsync(TimeSpan.FromMinutes(5), name: "wait-1");
        await ctx.WaitAsync(TimeSpan.FromHours(1), name: "wait-2");
        return new MultipleWaitsResult(CompletedWaits: 2, FinalStep: "done");
    }
}

public record MultipleWaitsResult(int CompletedWaits, string FinalStep);

See also

Checkpoint consumption

Durable operations consume checkpoints. To understand how this operation affects your checkpoint usage, see Checkpoint consumption.