Skip to content

Wait for Condition

Wait on a condition with polling

The Wait for Condition operation polls a check function on a schedule until your code signals it to stop. Behind the scenes, the check condition runs as a durable step, and each polling attempt is a retry, so the SDK checkpoints results automatically and tracks state between attempts. The function suspends between attempts and does not consume Lambda execution time.

It manages the entire check-and-wait cycle for you, including state tracking, backoff, and attempt counting.

Use waitForCondition when you need to poll until a condition is met. For example, if you need to poll an external system until something changes, like a batch job completing, a resource becoming available, or a status reaching a terminal state.

If the external system will send a notification or response, use Callbacks instead to suspend the durable function until it receives the response.

Walkthrough

Here's a simple example that polls until a job completes:

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Poll until job completes
    const result = await context.waitForCondition(
      "wait_for_job",
      async (state: { jobId: string; status: string; done: boolean }) => {
        const status = await getJobStatus(state.jobId);
        return { ...state, status, done: status === "COMPLETED" };
      },
      {
        initialState: { jobId: "job-123", status: "pending", done: false },
        waitStrategy: (state, attempt) => {
          if (state.done) {
            return { shouldContinue: false };
          }
          return {
            shouldContinue: true,
            delay: { seconds: Math.min(5 * Math.pow(2, attempt), 300) },
          };
        },
      },
    );
    return result;
  },
);
from aws_durable_execution_sdk_python import DurableContext, durable_execution
from aws_durable_execution_sdk_python.waits import WaitForConditionConfig, WaitForConditionDecision
from aws_durable_execution_sdk_python.config import Duration


def check_job_status(state, check_context):
    status = get_job_status(state["job_id"])
    return {
        "job_id": state["job_id"],
        "status": status,
        "done": status == "COMPLETED"
    }


def wait_strategy(state, attempt):
    if state["done"]:
        return WaitForConditionDecision.stop_polling()
    delay = min(5 * (2 ** attempt), 300)
    return WaitForConditionDecision.continue_waiting(Duration.from_seconds(delay))


@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
    result = context.wait_for_condition(
        check=check_job_status,
        config=WaitForConditionConfig(
            wait_strategy=wait_strategy,
            initial_state={"job_id": "job-123", "done": False},
        )
    )
    return result
import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
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;

public class WaitForConditionExample extends DurableHandler<Object, Map<String, Object>> {

    @Override
    public Map<String, Object> handleRequest(Object input, DurableContext context) {
        var strategy = WaitStrategies.<Map<String, Object>>exponentialBackoff(
                60, Duration.ofSeconds(5), Duration.ofMinutes(5), 2.0, JitterStrategy.NONE);

        var initialState = Map.<String, Object>of(
                "jobId", "job-123", "status", "pending", "done", false);

        var config = WaitForConditionConfig.<Map<String, Object>>builder()
                .initialState(initialState)
                .waitStrategy(strategy)
                .build();

        return context.waitForCondition(
                "wait_for_job",
                new TypeToken<>() {},
                (state, stepCtx) -> {
                    var status = getJobStatus((String) state.get("jobId"));
                    var updatedState = Map.<String, Object>of(
                            "jobId", state.get("jobId"),
                            "status", status,
                            "done", "COMPLETED".equals(status));
                    if (Boolean.TRUE.equals(updatedState.get("done"))) {
                        return WaitForConditionResult.stopPolling(updatedState);
                    }
                    return WaitForConditionResult.continuePolling(updatedState);
                },
                config);
    }
}

The check receives the durable.StepContext first, then the state, and returns the next state. The stop decision is in the strategy, not the check.

package main

import (
    "time"

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

type JobState struct {
    JobID  string `json:"jobId"`
    Status string `json:"status"`
    Done   bool   `json:"done"`
}

// getJobStatus stands in for a call to the job service. It reports the job
// complete on the second check.
func getJobStatus(sc durable.StepContext, jobID string) (string, error) {
    if sc.Attempt() < 2 {
        return "RUNNING", nil
    }
    return "COMPLETED", nil
}

func handler(ctx durable.Context, event map[string]any) (JobState, error) {
    // Poll until the job completes.
    return durable.WaitForCondition(ctx, "wait_for_job",
        func(sc durable.StepContext, state JobState) (JobState, error) {
            status, err := getJobStatus(sc, state.JobID)
            if err != nil {
                return state, err
            }
            state.Status = status
            state.Done = status == "COMPLETED"
            return state, nil
        },
        durable.ConditionConfig[JobState]{
            InitialState: JobState{JobID: "job-123", Status: "pending", Done: false},
            WaitStrategy: func(state JobState, attempt int) durable.WaitDecision {
                if state.Done {
                    return durable.WaitDecision{Continue: false}
                }
                return durable.WaitDecision{
                    Continue: true,
                    Delay:    time.Duration(min(5*(1<<attempt), 300)) * time.Second,
                }
            },
        },
    )
}

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

The check receives the durable::StepContext first, then the state, and returns the next state. The wait strategy decides when to stop.

use std::time::Duration;

use aws_durable_execution_sdk as durable;
use durable::builders::wait_for_condition::WaitDecision;
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct JobState {
    job_id: String,
    status: String,
    done: bool,
}

fn get_job_status(step_ctx: &durable::StepContext, _job_id: &str) -> String {
    let status = if step_ctx.attempt() < 2 {
        "RUNNING"
    } else {
        "COMPLETED"
    };
    status.to_owned()
}

async fn handler(
    _event: serde_json::Value,
    ctx: durable::DurableContext,
) -> Result<JobState, durable::BoxError> {
    let result = ctx
        .wait_for_condition(
            |step_ctx, state: JobState| async move {
                let status = get_job_status(&step_ctx, &state.job_id);
                let done = status == "COMPLETED";
                Ok(JobState {
                    status,
                    done,
                    ..state
                })
            },
            JobState {
                job_id: "job-123".to_owned(),
                status: "pending".to_owned(),
                done: false,
            },
        )
        .name("wait_for_job")
        .wait_strategy_fn(|state: JobState, attempt| {
            if state.done {
                return WaitDecision::complete();
            }
            WaitDecision::continue_with(Duration::from_secs(
                5_u64.saturating_mul(2_u64.saturating_pow(attempt)).min(300),
            ))
        })
        .await?;
    Ok(result)
}

#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
    durable::init_default_subscriber()?;
    durable::run(handler).await
}
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;

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

    private async Task<JobState> Workflow(object input, IDurableContext ctx)
    {
        // Poll until the job completes.
        var result = await ctx.WaitForConditionAsync(
            check: async (state, checkCtx, ct) =>
            {
                var status = await GetJobStatus(state.JobId, ct);
                return state with { Status = status, Done = status == "COMPLETED" };
            },
            config: new WaitForConditionConfig<JobState>
            {
                InitialState = new JobState("job-123", "pending", Done: false),
                WaitStrategy = WaitStrategy.Exponential<JobState>(
                    maxAttempts: 60,
                    initialDelay: TimeSpan.FromSeconds(5),
                    maxDelay: TimeSpan.FromMinutes(5),
                    backoffRate: 2.0,
                    jitter: JitterStrategy.None,
                    isDone: state => state.Done),
            },
            name: "wait_for_job");
        return result;
    }

    private static Task<string> GetJobStatus(string jobId, CancellationToken ct)
        => Task.FromResult("COMPLETED");
}

public record JobState(string JobId, string Status, bool Done);

Timing and order of operations

Each attempt runs the check function first, then applies the wait. The SDK runs the first check immediately, with no delay before it.

An attempt proceeds in this order:

  1. The SDK runs the check function with the current state. The first attempt receives the initialState you configured. Each later attempt receives the state the previous check returned.
  2. The SDK evaluates whether the condition is met. If it is, waitForCondition checkpoints the final state and returns it.
  3. If the condition is not met, the SDK checkpoints the delay for this attempt, suspends the function, and resumes for the next attempt once the delay elapses.

Where you express the stop-or-continue decision differs by language. See Custom strategy.

graph TD
    A[Start with initial state] --> B["check (step)"]
    B --> D{wait strategy}
    D -->|Continue| E[Suspend for delay]
    E -->|step retry| B
    D -->|Stop| F[Return final state]

The attempt counter starts at 1 and increments by one on each new attempt. The wait strategy receives this number, so it can grow the delay or cap the total attempts.

Because the check runs before the wait, no delay precedes the first check. With the strategy builder helper, initialDelay sets the wait after the first unmet check, before the second attempt. Each later wait grows from there: attempt n waits initialDelay * backoffRate^(n - 1), capped at maxDelay, with jitter applied.

Method signature

// context.waitForCondition()
waitForCondition<T>(
  name: string | undefined,
  checkFunc: WaitForConditionCheckFunc<T>,
  config: WaitForConditionConfig<T>,
): DurablePromise<T>;

waitForCondition<T>(
  checkFunc: WaitForConditionCheckFunc<T>,
  config: WaitForConditionConfig<T>,
): DurablePromise<T>;

// Check function
type WaitForConditionCheckFunc<T> = (
  state: T,
  context: WaitForConditionContext,
) => Promise<T>;

// WaitForConditionContext — provides a logger for use during checks
interface WaitForConditionContext {
  logger: DurableLogger;
}

// Config
interface WaitForConditionConfig<T> {
  waitStrategy: WaitForConditionWaitStrategyFunc<T>;
  initialState: T;
  serdes?: Serdes<T>;
}
# context.wait_for_condition()
def wait_for_condition(
    self,
    check: Callable[[T, WaitForConditionCheckContext], T],
    config: WaitForConditionConfig[T],
    name: str | None = None,
) -> T

# Check function
Callable[[T, WaitForConditionCheckContext], T]

# WaitForConditionCheckContext — passed to each check
class WaitForConditionCheckContext:
    logger: LoggerInterface
    attempt: int  # current attempt number, starting at 1

# Config
@dataclass
class WaitForConditionConfig(Generic[T]):
    wait_strategy: Callable[[T, int], WaitForConditionDecision]
    initial_state: T
    serdes: SerDes | None = None
// context.waitForCondition() — sync
<T> T waitForCondition(
    String name, Class<T> resultType,
    BiFunction<T, StepContext, WaitForConditionResult<T>> checkFunc);

<T> T waitForCondition(
    String name, Class<T> resultType,
    BiFunction<T, StepContext, WaitForConditionResult<T>> checkFunc,
    WaitForConditionConfig<T> config);

// context.waitForConditionAsync() — async
<T> DurableFuture<T> waitForConditionAsync(
    String name, Class<T> resultType,
    BiFunction<T, StepContext, WaitForConditionResult<T>> checkFunc,
    WaitForConditionConfig<T> config);

// Check function — returns WaitForConditionResult with isDone flag
BiFunction<T, StepContext, WaitForConditionResult<T>>

// WaitForConditionResult
public record WaitForConditionResult<T>(T value, boolean isDone) {
    public static <T> WaitForConditionResult<T> stopPolling(T value);
    public static <T> WaitForConditionResult<T> continuePolling(T value);
}

// Config
WaitForConditionConfig.<T>builder()
    .waitStrategy(strategy)     // optional, defaults to WaitStrategies.defaultStrategy()
    .initialState(initialState) // optional, defaults to null
    .serDes(serDes)             // optional
    .build();

The configuration is a ConditionConfig[S] struct passed positionally. cfg.WaitStrategy is required. A nil strategy makes WaitForCondition return an error at the call, before any check runs.

In a check, StepContext.Attempt() returns the 1-based poll attempt, the same number the wait strategy receives. A check that returns an error fails the operation at once, with no retry. WaitForCondition returns a *durable.WaitForConditionError when the check returns an error and when the strategy stops with Err set. A negative Delay makes it return an error.

func WaitForCondition[S any](ctx Context, name string, check func(StepContext, S) (S, error), cfg ConditionConfig[S]) (S, error)

type ConditionConfig[S any] struct {
    // InitialState is the state passed to the first check.
    InitialState S

    // WaitStrategy decides, after each check, whether to keep waiting and
    // for how long. attempt is the 1-based number of completed checks.
    // The strategy must be a deterministic function of its arguments,
    // except for randomized jitter in the returned delay.
    //
    // WaitStrategy is required and must not be nil. When it is nil,
    // [WaitForCondition] returns an error at the call and runs no check.
    WaitStrategy func(state S, attempt int) WaitDecision

    // Serdes overrides the serializer for the condition state.
    Serdes Serdes
}

initial_state is the second argument of wait_for_condition. Set the operation name with .name(...). The name is part of replay identity, so it must stay the same each time the handler replays. Set the strategy with .wait_strategy(...), which takes a WaitStrategy, or with .wait_strategy_fn(...), which takes a function. A strategy is required. Only the builder that one of the two methods returns can be awaited or turned into a future with .future() or .spawn(), so code without a strategy fails to compile. A second call replaces the first strategy. The SDK round-trips the initial state through the serdes before the first check. So with the default durable::JsonSerdes, the state type needs Clone, Serialize, and DeserializeOwned.

In a check, StepContext::attempt() returns the 1-based poll attempt, the same number the wait strategy receives. A check that returns Err fails the operation at once, without a call to the strategy. .await returns a durable::OperationError of kind OperationErrorKind::WaitForCondition, whose kind() is a durable::WaitForConditionErrorKind:

  • CheckFailed when the check returns Err.
  • MaxChecksExceeded(details) when the strategy returns WaitDecision::exhausted(reason). details.checks() returns the number of checks performed.
  • SerializationFailed when the state cannot be serialized or deserialized.
impl DurableContext {
    // ...
    pub fn wait_for_condition<S, F, Fut>(
        &self,
        check: F,
        initial_state: S,
    ) -> WaitForConditionBuilder<S, F, Fut>
    where
        F: Fn(StepContext, S) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = Result<S, BoxError>> + Send + 'static,
        S: Clone + Send + Sync + 'static;
    // ...
}
// ...

pub struct WaitForConditionBuilder<S, F, Fut, SD = JsonSerdes, WS = NoWaitStrategy> {
    // ...
}

impl<S, F, Fut, SD, WS> WaitForConditionBuilder<S, F, Fut, SD, WS>
where
    S: Clone + Send + Sync + 'static,
    F: Fn(StepContext, S) -> Fut + Send + Sync + 'static,
    Fut: Future<Output = Result<S, BoxError>> + Send + 'static,
{
    pub fn name(mut self, name: impl Into<String>) -> Self;
    // ...
    pub fn wait_strategy(
        self,
        strategy: WaitStrategy<S>,
    ) -> WaitForConditionBuilder<S, F, Fut, SD, WithWaitStrategy<S>>;
    // ...
    pub fn wait_strategy_fn<W>(
        self,
        strategy: W,
    ) -> WaitForConditionBuilder<S, F, Fut, SD, WithWaitStrategy<S>>
    where
        W: Fn(S, u32) -> WaitDecision + Send + Sync + 'static;
    // ...
    pub fn serdes<SD2>(self, serdes: SD2) -> WaitForConditionBuilder<S, F, Fut, SD2, WS>
    where
        SD2: Serdes<S>;
    // ...
}

impl<S, F, Fut, SD> WaitForConditionBuilder<S, F, Fut, SD, WithWaitStrategy<S>>
where
    S: Clone + Send + Sync + 'static,
    F: Fn(StepContext, S) -> Fut + Send + Sync + 'static,
    Fut: Future<Output = Result<S, BoxError>> + Send + 'static,
    SD: Serdes<S>,
{
    pub fn future(self) -> DurableFuture<S>;
    // ...
    pub fn spawn(self) -> DurableFuture<S>;
    // ...
}

impl<S, F, Fut, SD> IntoFuture for WaitForConditionBuilder<S, F, Fut, SD, WithWaitStrategy<S>>
// ...
{
    type Output = Result<S, OperationError>;
    // ...
}
// ctx.WaitForConditionAsync()
Task<TState> WaitForConditionAsync<TState>(
    Func<TState, IConditionCheckContext, CancellationToken, Task<TState>> check,
    WaitForConditionConfig<TState> config,
    string? name = null,
    CancellationToken cancellationToken = default);

// Check function
Func<TState, IConditionCheckContext, CancellationToken, Task<TState>>

// IConditionCheckContext — provides a logger and the 1-based attempt number
public interface IConditionCheckContext
{
    ILogger Logger { get; }
    int AttemptNumber { get; }
}

// Config (required; generic on the state type)
public sealed class WaitForConditionConfig<TState>
{
    public required TState InitialState { get; set; }
    public required IWaitStrategy<TState> WaitStrategy { get; set; }
}

Parameters:

  • name (optional) - Only used for display, debugging and testing.
  • check (required) - A function that performs a status check. The check function runs as a durable step and each subsequent polling attempt is a retry, so avoid heavy computation or side effects and keep it focused on querying status.
  • config - A configuration object containing:
    • initialState - The state object passed to the first check invocation. In Python the SDK round-trips this value through the configured SerDes before the first check, so it must be serializable by that SerDes.
    • waitStrategy - A Wait strategy to control polling behavior

Returns: The final state object from the last check function invocation.

Wait strategies

The wait strategy controls how long to wait between polling attempts and when to stop polling. You can write your own custom strategy from scratch, or you can use the strategy builder helper to create a strategy for you based on configuration values you provide.

Wait strategy signature

// Wait strategy
type WaitForConditionWaitStrategyFunc<T> = (
  state: T,
  attempt: number,
) => WaitForConditionDecision;

// Decision
type WaitForConditionDecision =
  | { shouldContinue: true; delay: Duration }
  | { shouldContinue: false };
# Wait strategy
Callable[[T, int], WaitForConditionDecision]

# Decision
@dataclass
class WaitForConditionDecision:
    should_continue: bool
    delay: Duration

    @classmethod
    def continue_waiting(cls, delay: Duration) -> WaitForConditionDecision: ...

    @classmethod
    def stop_polling(cls) -> WaitForConditionDecision: ...
// Wait strategy — returns the delay Duration before the next polling attempt
@FunctionalInterface
public interface WaitForConditionWaitStrategy<T> {
    Duration evaluate(T state, int attempt);
}

The strategy returns a WaitDecision struct. To poll again, set Continue and a Delay. To stop and succeed, leave Continue false and Err nil. To stop and fail, leave Continue false and set Err.

type WaitStrategy[S any] func(state S, attempt int) WaitDecision

type WaitDecision struct {
    // Continue indicates whether to keep waiting and check again.
    Continue bool

    // Delay is how long to suspend before the next check. A Delay of 0 is
    // raised to one second. A positive Delay is rounded up to whole
    // seconds, so a positive Delay under one second waits one second. A
    // negative Delay makes WaitForCondition return an error. Delay is
    // ignored when Continue is false.
    Delay time.Duration

    // Err, when non-nil and Continue is false, signals that the
    // operation should fail with this error rather than succeed. Use it
    // to implement max-attempts or timeout strategies.
    Err error
}

The strategy receives the state and the 1-based attempt, and returns a durable::builders::wait_for_condition::WaitDecision.

  • WaitDecision::complete() stops polling and returns the state.
  • WaitDecision::continue_with(delay) polls again after delay, rounded up to whole seconds with a minimum of one second.
  • WaitDecision::exhausted(reason) stops polling and fails the operation with WaitForConditionErrorKind::MaxChecksExceeded. The record carries the error type "WaitForCondition.MaxChecksExceeded" and the message reason.
pub fn wait_strategy_fn<W>(
    self,
    strategy: W,
) -> WaitForConditionBuilder<S, F, Fut, SD, WithWaitStrategy<S>>
where
    W: Fn(S, u32) -> WaitDecision + Send + Sync + 'static;
// ...

pub enum WaitDecision {
    Complete,
    #[non_exhaustive]
    Continue {
        delay: Duration,
    },
    #[non_exhaustive]
    Exhausted {
        reason: String,
    },
}

impl WaitDecision {
    pub fn complete() -> Self;
    // ...
    pub fn continue_with(delay: Duration) -> Self;
    // ...
    pub fn exhausted(reason: impl Into<String>) -> Self;
    // ...
}
// Wait strategy — decides, per poll, whether to continue and how long to wait
public interface IWaitStrategy<TState>
{
    WaitDecision Decide(TState state, int attemptNumber);
}

// Decision
public readonly record struct WaitDecision
{
    public bool ShouldContinue { get; }
    public TimeSpan Delay { get; }

    public static WaitDecision Stop();                        // condition met
    public static WaitDecision ContinueAfter(TimeSpan delay); // poll again after delay
}

Custom strategy

Write your own strategy function for full control over polling behavior. The function receives the current state and attempt number. The strategy function decides whether to continue polling or not.

const strategy = (state: { status: string }, attempt: number) => {
  if (state.status === "COMPLETED") {
    return { shouldContinue: false };
  }
  if (attempt >= 10) {
    throw new Error("Max attempts exceeded");
  }
  return { shouldContinue: true, delay: { seconds: attempt * 5 } };
};
from aws_durable_execution_sdk_python.waits import WaitForConditionDecision
from aws_durable_execution_sdk_python.config import Duration


def strategy(state, attempt):
    if state["status"] == "COMPLETED":
        return WaitForConditionDecision.stop_polling()
    if attempt >= 10:
        raise ValueError("Max attempts exceeded")
    return WaitForConditionDecision.continue_waiting(Duration.from_seconds(attempt * 5))

In Java, the stop/continue decision lives in the check function (via WaitForConditionResult). The strategy only computes the delay.

WaitForConditionWaitStrategy<Map<String, String>> strategy = (state, attempt) -> {
    if (attempt >= 10) {
        throw new WaitForConditionFailedException("Max attempts exceeded");
    }
    return Duration.ofSeconds(attempt * 5);
};

To stop and fail, return a WaitDecision with Err set and Continue false, or return an error from the check.

strategy := func(state JobState, attempt int) durable.WaitDecision {
    if state.Status == "COMPLETED" {
        return durable.WaitDecision{Continue: false}
    }
    if attempt >= 10 {
        return durable.WaitDecision{Continue: false, Err: errors.New("max attempts exceeded")}
    }
    return durable.WaitDecision{Continue: true, Delay: time.Duration(attempt) * 5 * time.Second}
}

To stop and fail, return WaitDecision::exhausted(reason) from the strategy, or return Err from the check. Pass the strategy with .wait_strategy_fn(strategy()). JobState is the state type from the walkthrough.

use std::time::Duration;

use aws_durable_execution_sdk as durable;
use durable::builders::wait_for_condition::WaitDecision;
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct JobState {
    job_id: String,
    status: String,
    done: bool,
}

fn strategy() -> impl Fn(JobState, u32) -> WaitDecision {
    |state, attempt| {
        if state.status == "COMPLETED" {
            return WaitDecision::complete();
        }
        if attempt >= 10 {
            return WaitDecision::exhausted("Max attempts exceeded");
        }
        WaitDecision::continue_with(Duration::from_secs(u64::from(attempt) * 5))
    }
}

Implement IWaitStrategy<TState> or wrap a delegate with WaitStrategy.FromDelegate. The strategy returns a WaitDecision: WaitDecision.Stop() to stop polling or WaitDecision.ContinueAfter(delay) to poll again.

using Amazon.Lambda.DurableExecution;

public record JobState(string Status);

public static class CustomWaitStrategy
{
    // Full control over polling: the strategy decides whether to continue or
    // stop, and how long to wait before the next attempt.
    public static IWaitStrategy<JobState> Create() =>
        WaitStrategy.FromDelegate<JobState>((state, attempt) =>
        {
            if (state.Status == "COMPLETED")
            {
                return WaitDecision.Stop();
            }
            if (attempt >= 10)
            {
                throw new WaitForConditionException("Max attempts exceeded");
            }
            return WaitDecision.ContinueAfter(TimeSpan.FromSeconds(attempt * 5));
        });
}

To stop polling and signal an error, throw an exception from the strategy or the check function.

Strategy builder helper

Use the ready-made wait strategy helper to generate a strategy function from common parameters like backoff rate, max attempts, and jitter. This is a convenience so you can re-use common wait strategy logic without having to code your own function.

import { createWaitStrategy } from "@aws/durable-execution-sdk-js";

const strategy = createWaitStrategy({
  shouldContinuePolling: (state) => state.status !== "COMPLETED",
  maxAttempts: 10,
  initialDelay: { seconds: 5 },
  maxDelay: { minutes: 5 },
  backoffRate: 2.0,
  jitter: JitterStrategy.FULL,
});
from aws_durable_execution_sdk_python.waits import create_wait_strategy, WaitStrategyConfig, JitterStrategy
from aws_durable_execution_sdk_python.config import Duration

strategy = create_wait_strategy(WaitStrategyConfig(
    should_continue_polling=lambda state: state["status"] != "COMPLETED",
    max_attempts=10,
    initial_delay=Duration.from_seconds(5),
    max_delay=Duration.from_minutes(5),
    backoff_rate=2.0,
    jitter_strategy=JitterStrategy.FULL,
))
import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.WaitStrategies;

var defaultStrategy = WaitStrategies.<Map<String, String>>defaultStrategy();

var exponentialStrategy = WaitStrategies.<Map<String, String>>exponentialBackoff(
        10,                          // maxAttempts
        Duration.ofSeconds(5),       // initialDelay
        Duration.ofMinutes(5),       // maxDelay
        2.0,                         // backoffRate
        JitterStrategy.FULL);        // jitter

var fixedDelayStrategy = WaitStrategies.<Map<String, String>>fixedDelay(
        10,                          // maxAttempts
        Duration.ofSeconds(30));     // fixedDelay

WaitStrategies.defaultStrategy() returns the same strategy as WaitStrategies.Presets.DEFAULT: 60 maximum attempts, a 5-second initial delay, a 5-minute maximum delay, a 1.5x backoff rate, and full jitter.

Use WaitStrategies.exponentialBackoff(...) to customize each backoff parameter. Use WaitStrategies.fixedDelay(maxAttempts, delay) for a constant delay between checks. Both factories count attempts from 1 and throw WaitForConditionFailedException when the check remains incomplete after maxAttempts.

Java's strategy factories do not accept shouldContinuePolling. The check function controls completion by returning WaitForConditionResult.stopPolling(...) or WaitForConditionResult.continuePolling(...).

strategy := durable.MustNewWaitStrategy(durable.WaitConfig[JobState]{
    MaxAttempts:    10,
    InitialDelay:   5 * time.Second,
    MaxDelay:       5 * time.Minute,
    BackoffRate:    2.0,
    Jitter:         durable.JitterFull,
    ShouldContinue: func(state JobState) bool { return state.Status != "COMPLETED" },
})

ShouldContinue is the shouldContinuePolling predicate. It returns false when the condition is met. When ShouldContinue is nil, the strategy never stops early, so the operation can only fail at MaxAttempts. The jitter constants are durable.JitterFull, durable.JitterHalf, and durable.JitterNone. For a constant delay, set BackoffRate to 1.

A zero WaitConfig field selects the default listed below. So MaxAttempts: 0 means 60 checks. For one check, set MaxAttempts to 1. InitialDelay and MaxDelay must be at least one second when set. BackoffRate must be finite and not negative. Jitter must be one of the jitter constants. durable.NewWaitStrategy returns an error for an invalid configuration, and durable.MustNewWaitStrategy panics. When the strategy reaches MaxAttempts with the condition unmet, the operation fails with a *durable.WaitForConditionError.

use std::time::Duration;

use aws_durable_execution_sdk as durable;
use durable::builders::JitterStrategy;
use durable::builders::wait_for_condition::WaitStrategy;
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct JobState {
    job_id: String,
    status: String,
    done: bool,
}

fn strategy() -> WaitStrategy<JobState> {
    WaitStrategy::builder(|state: &JobState| state.status == "COMPLETED")
        .max_attempts(10)
        .initial_delay(Duration::from_secs(5))
        .max_delay(Duration::from_mins(5))
        .backoff_rate(2.0)
        .jitter(JitterStrategy::Full)
        .build()
}

WaitStrategy::builder takes a completion predicate in place of shouldContinuePolling. The predicate returns true when the condition is met, and polling stops when it does. Pass the built strategy to .wait_strategy(strategy()). The jitter values are durable::builders::JitterStrategy::Full, Half, and None.

A method you do not call keeps the default listed below. The SDK does not check the values. A computed delay is rounded to the nearest second, with a minimum of one second. When the strategy reaches max_attempts with the condition unmet, the operation fails with WaitForConditionErrorKind::MaxChecksExceeded.

using Amazon.Lambda.DurableExecution;

public record JobState(string Status);

public static class StrategyHelper
{
    // Build an exponential-backoff strategy from common parameters. The
    // isDone predicate stops polling once the latest state satisfies it.
    public static IWaitStrategy<JobState> Create() =>
        WaitStrategy.Exponential<JobState>(
            maxAttempts: 10,
            initialDelay: TimeSpan.FromSeconds(5),
            maxDelay: TimeSpan.FromMinutes(5),
            backoffRate: 2.0,
            jitter: JitterStrategy.Full,
            isDone: state => state.Status == "COMPLETED");
}

Helper parameters:

  • shouldContinuePolling - Predicate that returns true to keep polling or false to stop
  • maxAttempts - Maximum polling attempts. Set this to prevent runaway executions. Defaults to 60. When the helper strategy reaches this limit before the condition is met, it fails the operation. In Python the SDK raises WaitForConditionError.
  • initialDelay - Delay before the first retry. Defaults to 5 seconds
  • maxDelay - Maximum delay between attempts. Defaults to 5 minutes
  • backoffRate - Multiplier applied to the delay after each attempt (e.g., 2.0 doubles the delay). Defaults to 1.5
  • jitter - Jitter strategy (FULL, HALF, or NONE). Defaults to FULL

Delays between attempts are approximate. The actual time depends on system scheduling, Lambda cold start time, and current system load.

See also

Checkpoint consumption

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