Retry Strategies¶
Retry suspends invocation¶
When a step throws an exception, the SDK uses the step's retry strategy to define the retry behaviour. When the strategy logic requires a retry, the SDK checkpoints the error and the scheduled resume time, and then ends the Lambda invocation. The backend starts a new invocation for the execution at the scheduled resume time and the SDK replays the step body.
Retries do not consume Lambda execution time while waiting for the next retry.
When a step exhausts all retry attempts, the SDK checkpoints the final error and throws it to your handler. If you configure no retry strategy on a step, the SDK applies a default strategy with up to 5 retries (6 total attempts). See Retry presets.
Configure a retry strategy¶
A retry strategy is a function that takes the error and the current attempt number, and returns a decision. The decision is either to retry with a given delay, or to stop. You can write a retry strategy directly yourself or use one of the built-in helpers to build a ready-made strategy from configuration. The SDK ships helpers for exponential backoff and linear backoff.
Exponential backoff¶
Use createRetryStrategy() to build a strategy, then pass it as retryStrategy in
StepConfig.
import {
withDurableExecution,
createRetryStrategy,
StepConfig,
} from "@aws/durable-execution-sdk-js";
const retryStrategy = createRetryStrategy({
maxAttempts: 5,
initialDelay: { seconds: 2 },
maxDelay: { minutes: 1 },
backoffRate: 2,
});
const stepConfig: StepConfig<string> = { retryStrategy };
export const handler = withDurableExecution(async (event, context) => {
const result = await context.step(
"call-external-api",
async () => callExternalApi(),
stepConfig,
);
return result;
});
async function callExternalApi(): Promise<string> {
return "ok";
}
Use create_retry_strategy() with a RetryStrategyConfig, then pass it as
retry_strategy in StepConfig.
from aws_durable_execution_sdk_python.config import Duration, StepConfig
from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python.retries import RetryStrategyConfig, create_retry_strategy
retry_strategy = create_retry_strategy(
RetryStrategyConfig(
max_attempts=5,
initial_delay=Duration.from_seconds(2),
max_delay=Duration.from_minutes(1),
backoff_rate=2.0,
)
)
step_config = StepConfig(retry_strategy=retry_strategy)
@durable_step
def call_external_api(step_context: StepContext) -> str:
return "ok"
@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> str:
result = context.step(call_external_api(), config=step_config)
return result
Use RetryStrategies.exponentialBackoff() to build a strategy, then pass it to
StepConfig.builder().retryStrategy().
import java.time.Duration;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.RetryStrategies;
public class ExponentialBackoffExample extends DurableHandler<Object, String> {
@Override
public String handleRequest(Object input, DurableContext context) {
StepConfig config = StepConfig.builder()
.retryStrategy(RetryStrategies.exponentialBackoff(
3,
Duration.ofSeconds(1),
Duration.ofSeconds(10),
2.0,
JitterStrategy.FULL))
.build();
String result = context.step("retry_step", String.class,
(StepContext ctx) -> "Step with exponential backoff",
config);
return "Result: " + result;
}
}
Build a strategy with MustNewRetryStrategy and a RetryConfig, then pass it with
the WithRetry step option. MustNewRetryStrategy panics for an invalid config.
NewRetryStrategy returns an error instead. The default strategy is
ExponentialBackoff.
package main
import (
"time"
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
var retryStrategy = durable.MustNewRetryStrategy(durable.RetryConfig{
MaxAttempts: 5,
InitialDelay: 2 * time.Second,
MaxDelay: 1 * time.Minute,
BackoffRate: 2,
})
func handler(ctx durable.Context, _ any) (string, error) {
return durable.Step(ctx, "call-external-api",
func(durable.StepContext) (string, error) {
return callExternalAPI()
},
durable.WithRetry(retryStrategy),
)
}
func callExternalAPI() (string, error) {
return "ok", nil
}
func main() { durable.Start(handler) }
Build a durable::builders::RetryStrategyConfig with
RetryStrategyConfig::builder(), then pass it to the step with
.retry_strategy_config(..). build() cannot fail. The invocation suspends during
a retry delay when all remaining SDK work is blocked. A retry that becomes due
while SDK work continues starts in the same invocation.
use aws_durable_execution_sdk as durable;
use durable::builders::RetryStrategyConfig;
use std::time::Duration;
async fn handler(
_event: serde_json::Value,
ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
let retry_strategy = RetryStrategyConfig::builder()
.max_attempts(5)
.initial_delay(Duration::from_secs(2))
.max_delay(Duration::from_mins(1))
.backoff_rate(2.0)
.build();
let result = ctx
.step(|_| async { Ok(call_external_api()) })
.name("call-external-api")
.retry_strategy_config(retry_strategy)
.await?;
Ok(result)
}
fn call_external_api() -> String {
"ok".to_owned()
}
#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
durable::init_default_subscriber()?;
durable::run(handler).await
}
Use RetryStrategy.Exponential(...) to build an IRetryStrategy, then set it on
StepConfig.RetryStrategy.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
public class ExponentialBackoffExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
private async Task<string> Workflow(object input, IDurableContext ctx)
{
var config = new StepConfig
{
RetryStrategy = RetryStrategy.Exponential(
maxAttempts: 3,
initialDelay: TimeSpan.FromSeconds(1),
maxDelay: TimeSpan.FromSeconds(10),
backoffRate: 2.0,
jitter: JitterStrategy.Full),
};
string result = await ctx.StepAsync(
async (_, _) => "Step with exponential backoff",
name: "retry_step",
config: config);
return $"Result: {result}";
}
}
RetryStrategyConfig signature¶
import { JitterStrategy, Duration } from "@aws/durable-execution-sdk-js";
export interface RetryStrategyConfig {
/** Default: 3 */
maxAttempts?: number;
/** Default: { seconds: 5 } */
initialDelay?: Duration;
/** Default: { minutes: 5 } */
maxDelay?: Duration;
/** Default: 2 */
backoffRate?: number;
/** Default: JitterStrategy.FULL */
jitter?: JitterStrategy;
retryableErrors?: (string | RegExp)[];
retryableErrorTypes?: (new () => Error)[];
}
Parameters:
maxAttempts(optional) Total attempts including the initial attempt. Default:3.initialDelay(optional) Delay before the first retry. Default:{ seconds: 5 }.maxDelay(optional) Maximum delay between retries. Default:{ minutes: 5 }.backoffRate(optional) Multiplier applied to the delay on each retry. Default:2.jitter(optional) AJitterStrategyvalue. Default:JitterStrategy.FULL.retryableErrors(optional) Array of strings orRegExppatterns matched against the error message. The SDK retries all errors when you set neitherretryableErrorsnorretryableErrorTypes.retryableErrorTypes(optional) Array of error classes. The SDK retries only errors that are instances of these classes. When you set both filters, the SDK retries an error if it matches either (OR logic).
import re
from dataclasses import dataclass
from aws_durable_execution_sdk_python.config import Duration, JitterStrategy
from aws_durable_execution_sdk_python.retries import RetryDecision
@dataclass
class RetryStrategyConfig:
max_attempts: int = 3
initial_delay: Duration = Duration.from_seconds(5)
max_delay: Duration = Duration.from_minutes(5)
backoff_rate: float = 2.0
jitter_strategy: JitterStrategy = JitterStrategy.FULL
retryable_errors: list[str | re.Pattern] | None = None
retryable_error_types: list[type[Exception]] | None = None
Parameters:
max_attempts(optional) Total attempts including the initial attempt. Default:3.initial_delay(optional) ADuration. Default:Duration.from_seconds(5).max_delay(optional) ADuration. Default:Duration.from_minutes(5).backoff_rate(optional) Multiplier applied to the delay on each retry. Default:2.0.jitter_strategy(optional) AJitterStrategyvalue. Default:JitterStrategy.FULL.retryable_errors(optional) List of strings or compiledre.Patternobjects matched against the error message. The SDK retries all errors when you set neitherretryable_errorsnorretryable_error_types.retryable_error_types(optional) List of exception classes. The SDK retries only exceptions that are instances of these classes. When you set both filters, the SDK retries an error if it matches either (OR logic).
RetryStrategy RetryStrategies.exponentialBackoff(
int maxAttempts,
Duration initialDelay,
Duration maxDelay,
double backoffRate,
JitterStrategy jitter
)
RetryStrategy RetryStrategies.fixedDelay(
int maxAttempts,
Duration fixedDelay
)
Parameters:
maxAttemptsTotal attempts including the initial attempt.initialDelayAjava.time.Duration. Minimum 1 second.maxDelayAjava.time.Duration. Minimum 1 second.backoffRateMultiplier applied to the delay on each retry.jitterAJitterStrategyvalue:FULL,HALF, orNONE.
Java does not have built-in error type filtering. Filter by error type manually inside
the RetryStrategy lambda. See Retrying specific errors.
The type is RetryConfig. The zero value of each field selects its default. The
zero value of the whole struct is not the ExponentialBackoff preset.
type RetryConfig struct {
// MaxAttempts is the maximum number of total attempts, including the
// first. The default is 3. It must not be negative.
MaxAttempts int
// InitialDelay is the delay before the first retry. The default is
// 5 seconds. When set, it must be at least 1 second.
InitialDelay time.Duration
// MaxDelay caps the delay between retries. The default is 5 minutes.
// When set, it must be at least 1 second.
MaxDelay time.Duration
// BackoffRate multiplies the delay after each attempt. The default
// is 2. It must be a finite value and must not be negative.
BackoffRate float64
// Jitter is the jitter strategy applied to computed delays. The
// default is [JitterFull]. When set, it must be one of the defined
// [JitterStrategy] constants.
Jitter JitterStrategy
// RetryableErrors restricts retries to errors that at least one
// matcher reports as retryable. When empty, every error is retryable.
// Entries must not be nil.
RetryableErrors []ErrorMatcher
// Has unexported fields.
}
Fields:
MaxAttemptsTotal attempts including the first. Default:3. Must not be negative. The zero value selects the default. UseNoRetry()orMaxAttempts: 1for a single attempt.InitialDelayAtime.Duration. Default:5 * time.Second. At least 1 second when set.MaxDelayAtime.Duration. Default:5 * time.Minute. At least 1 second when set.BackoffRateMultiplier applied to the delay on each retry. Default:2. Must be finite and not negative.JitterAJitterStrategyvalue. Default:JitterFull. When set, it must be one of the defined constants.RetryableErrorsA slice ofErrorMatcher. When empty, every error is retryable. Entries must not be nil. An error that no matcher accepts fails the step on that attempt. See Retrying specific errors.
NewRetryStrategy() returns one joined error that names every invalid field. The
strategy rounds each delay to the nearest whole second, with a minimum of 1 second.
Set each field with a method of RetryStrategyConfigBuilder, which
RetryStrategyConfig::builder() returns. build() fills every field you leave
unset with its default.
impl RetryStrategyConfigBuilder {
pub fn max_attempts(mut self, max_attempts: u32) -> Self;
// ...
pub fn initial_delay(mut self, delay: Duration) -> Self;
// ...
pub fn max_delay(mut self, delay: Duration) -> Self;
// ...
pub fn backoff_rate(self, rate: f64) -> Self;
// ...
pub fn backoff(mut self, backoff: Backoff) -> Self;
// ...
pub fn jitter(mut self, jitter: JitterStrategy) -> Self;
// ...
pub fn retry_if<P>(mut self, predicate: P) -> Self
where
P: Fn(&StepError) -> bool + Send + Sync + 'static;
// ...
pub fn build(self) -> RetryStrategyConfig;
// ...
}
Methods:
max_attemptsTotal attempts including the initial attempt. Default:3, or6with a linear backoff.initial_delayAstd::time::Duration. Default: 5 seconds, or 1 second with a linear backoff.max_delayADurationthat caps the delay before jitter. Default: 5 minutes.backoff_rateMultiplier applied to the delay on each retry. It setsBackoff::exponential(rate). Default:2.0.backoffABackoffvalue, exponential or linear.backoffandbackoff_rateset the same value, so the last call wins. Default:Backoff::exponential(2.0).jitterAJitterStrategyvalue. Default:JitterStrategy::Full.retry_ifA predicate over thedurable::StepError. The SDK retries only the errors for which it returnstrue. Without it, the SDK retries every error. See Retry only specific errors.
build() accepts any value. The strategy rounds each delay to the nearest whole
second, with a minimum of 1 second. RetryStrategyConfig::default_preset()
returns the default preset from Retry presets, which differs
from RetryStrategyConfig::builder().build().
// .NET has no config object; build strategies with the RetryStrategy factory.
public static IRetryStrategy RetryStrategy.Exponential(
int maxAttempts = 3,
TimeSpan? initialDelay = null, // default 5s
TimeSpan? maxDelay = null, // default 300s
double backoffRate = 2.0,
JitterStrategy jitter = JitterStrategy.Full,
Type[]? retryableExceptions = null,
string[]? retryableMessagePatterns = null);
Parameters:
maxAttempts(optional) Total attempts including the initial attempt. Default:3.initialDelay(optional) ATimeSpan. Must be positive. Default:TimeSpan.FromSeconds(5).maxDelay(optional) ATimeSpan. Must be positive and>= initialDelay. Default:TimeSpan.FromSeconds(300).backoffRate(optional) Multiplier applied to the delay on each retry. Must be>= 1.0. Default:2.0.jitter(optional) AJitterStrategyvalue. Default:JitterStrategy.Full.retryableExceptions(optional) An array of exceptionTypes. The SDK retries only exceptions assignable to one of these types.retryableMessagePatterns(optional) An array of regex strings matched against the exception message. When you set neither filter, the SDK retries all exceptions. When you set both, the SDK retries an exception if it matches either (OR logic).
JitterStrategy¶
type JitterStrategy string
const (
// JitterFull randomizes the delay between zero and the computed
// delay. This is the default.
JitterFull JitterStrategy = "FULL"
// JitterHalf randomizes the delay between half the computed delay
// and the computed delay.
JitterHalf JitterStrategy = "HALF"
// JitterNone applies the computed delay unchanged.
JitterNone JitterStrategy = "NONE"
)
RetryConfig and WaitConfig default to JitterFull. LinearRetryConfig defaults
to JitterNone.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum JitterStrategy {
None, // exact calculated delay
Half, // random between 50% and 100% of base_delay
#[default]
Full, // random between 0 and base_delay
}
A config from RetryStrategyConfig::builder() defaults to JitterStrategy::Full.
The RetryStrategyConfig::linear() preset uses JitterStrategy::None.
Delay calculation¶
The SDK calculates the delay before each retry using exponential backoff with jitter:
base_delay = min(initial_delay × backoff_rate ^ (attempt - 1), max_delay)
final_delay = jitter(base_delay), minimum 1 second
JitterStrategy.FULLrandomizes the delay between 0 andbase_delay. This spreads retries across time and avoids many clients retrying simultaneously after a shared failure.JitterStrategy.HALFrandomizes between 50% and 100% ofbase_delay.JitterStrategy.NONEuses the exact calculated delay.
Linear backoff¶
Linear backoff grows the delay by a fixed increment on each attempt instead of
multiplying by a backoff rate. Use it when you want predictable, bounded growth between
retries rather than the rapid expansion of exponential backoff.
Use createLinearRetryStrategy() to build a strategy, then pass it as
retryStrategy in StepConfig.
import {
withDurableExecution,
createLinearRetryStrategy,
StepConfig,
} from "@aws/durable-execution-sdk-js";
const retryStrategy = createLinearRetryStrategy({
maxAttempts: 5,
initialDelay: { seconds: 2 },
increment: { seconds: 3 },
maxDelay: { seconds: 30 },
});
const stepConfig: StepConfig<string> = { retryStrategy };
export const handler = withDurableExecution(async (event, context) => {
return context.step(
"call-external-api",
async () => callExternalApi(),
stepConfig,
);
});
async function callExternalApi(): Promise<string> {
return "ok";
}
Use create_linear_retry_strategy() with a LinearRetryStrategyConfig, then pass it
as retry_strategy in StepConfig.
from aws_durable_execution_sdk_python.config import Duration, StepConfig
from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python.retries import (
LinearRetryStrategyConfig,
create_linear_retry_strategy,
)
retry_strategy = create_linear_retry_strategy(
LinearRetryStrategyConfig(
max_attempts=5,
initial_delay=Duration.from_seconds(2),
increment=Duration.from_seconds(3),
max_delay=Duration.from_seconds(30),
)
)
step_config = StepConfig(retry_strategy=retry_strategy)
@durable_step
def call_external_api(step_context: StepContext) -> str:
return "ok"
@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> str:
return context.step(call_external_api(), config=step_config)
Use RetryStrategies.linearBackoff() to build a strategy, then pass it to
StepConfig.builder().retryStrategy().
import java.time.Duration;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.RetryStrategies;
public class LinearRetryStrategyExample extends DurableHandler<Object, String> {
@Override
public String handleRequest(Object input, DurableContext context) {
StepConfig config = StepConfig.builder()
.retryStrategy(RetryStrategies.linearBackoff(
5, // maxAttempts
Duration.ofSeconds(2), // initialDelay
Duration.ofSeconds(30), // maxDelay
Duration.ofSeconds(3), // increment
JitterStrategy.FULL)) // jitter
.build();
return context.step("call-external-api", String.class,
(StepContext ctx) -> "ok",
config);
}
}
Build a strategy with MustLinearBackoff and a LinearRetryConfig, then pass it
with WithRetry. MustLinearBackoff panics for an invalid config. LinearBackoff
returns an error instead. LinearBackoff defaults to no jitter, so this example
sets JitterFull.
package main
import (
"time"
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
var retryStrategy = durable.MustLinearBackoff(durable.LinearRetryConfig{
MaxAttempts: 5,
InitialDelay: 2 * time.Second,
Increment: 3 * time.Second,
MaxDelay: 30 * time.Second,
// LinearBackoff defaults to JitterNone, so set full jitter explicitly.
Jitter: durable.JitterFull,
})
func handler(ctx durable.Context, _ any) (string, error) {
return durable.Step(ctx, "call-external-api",
func(durable.StepContext) (string, error) {
return callExternalAPI()
},
durable.WithRetry(retryStrategy),
)
}
func callExternalAPI() (string, error) {
return "ok", nil
}
func main() { durable.Start(handler) }
Pass Backoff::linear(increment) to the builder's .backoff(..), then pass the
config to the step with .retry_strategy_config(..).
use aws_durable_execution_sdk as durable;
use durable::builders::{Backoff, RetryStrategyConfig};
use std::time::Duration;
async fn handler(
_event: serde_json::Value,
ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
let retry_strategy = RetryStrategyConfig::builder()
.max_attempts(5)
.initial_delay(Duration::from_secs(2))
.backoff(Backoff::linear(Duration::from_secs(3)))
.max_delay(Duration::from_secs(30))
.build();
let result = ctx
.step(|_| async { Ok(call_external_api()) })
.name("call-external-api")
.retry_strategy_config(retry_strategy)
.await?;
Ok(result)
}
fn call_external_api() -> String {
"ok".to_owned()
}
#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
durable::init_default_subscriber()?;
durable::run(handler).await
}
The .NET SDK ships no built-in linear strategy. Use RetryStrategy.FromDelegate and
compute a linearly growing delay yourself, then set it on StepConfig.RetryStrategy.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
public class LinearRetryStrategyExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
private async Task<string> Workflow(object input, IDurableContext ctx)
{
// .NET has no built-in linear strategy. Build one with RetryStrategy.FromDelegate:
// the delay grows by a fixed increment on each attempt, capped at maxDelay.
var maxAttempts = 5;
var initialDelay = TimeSpan.FromSeconds(2);
var increment = TimeSpan.FromSeconds(3);
var maxDelay = TimeSpan.FromSeconds(30);
var config = new StepConfig
{
RetryStrategy = RetryStrategy.FromDelegate((error, attempt) =>
{
if (attempt >= maxAttempts)
{
return RetryDecision.DoNotRetry();
}
// attempt is 1-based: initialDelay + increment * (attempt - 1), capped at maxDelay
var delay = initialDelay + TimeSpan.FromTicks(increment.Ticks * (attempt - 1));
if (delay > maxDelay)
{
delay = maxDelay;
}
return RetryDecision.RetryAfter(delay);
}),
};
return await ctx.StepAsync(
async (_, _) => "ok",
name: "call-external-api",
config: config);
}
}
LinearRetryStrategyConfig signature¶
import { JitterStrategy, Duration } from "@aws/durable-execution-sdk-js";
export interface LinearRetryStrategyConfig {
/** Default: 6 */
maxAttempts?: number;
/** Default: { seconds: 1 } */
initialDelay?: Duration;
/** Default: { seconds: 1 } */
increment?: Duration;
/** Default: { minutes: 5 } */
maxDelay?: Duration;
/** Default: JitterStrategy.FULL */
jitter?: JitterStrategy;
retryableErrors?: (string | RegExp)[];
retryableErrorTypes?: (new () => Error)[];
}
Parameters:
maxAttempts(optional) Total attempts including the initial attempt. Default:6.initialDelay(optional) Delay before the first retry. Default:{ seconds: 1 }.increment(optional) Amount added to the delay on each retry. Default:{ seconds: 1 }.maxDelay(optional) Maximum delay between retries. Default:{ minutes: 5 }.jitter(optional) AJitterStrategyvalue. Default:JitterStrategy.FULL.retryableErrors(optional) Array of strings orRegExppatterns matched against the error message. The SDK retries all errors when you set neitherretryableErrorsnorretryableErrorTypes.retryableErrorTypes(optional) Array of error classes. The SDK retries only errors that are instances of these classes. When you set both filters, the SDK retries an error if it matches either (OR logic).
import re
from dataclasses import dataclass
from aws_durable_execution_sdk_python.config import Duration, JitterStrategy
@dataclass
class LinearRetryStrategyConfig:
max_attempts: int = 6
initial_delay: Duration = Duration.from_seconds(1)
increment: Duration = Duration.from_seconds(1)
max_delay: Duration = Duration.from_minutes(5)
jitter_strategy: JitterStrategy = JitterStrategy.FULL
retryable_errors: list[str | re.Pattern] | None = None
retryable_error_types: list[type[Exception]] | None = None
Parameters:
max_attempts(optional) Total attempts including the initial attempt. Default:6.initial_delay(optional) ADuration. Default:Duration.from_seconds(1).increment(optional) Amount added to the delay on each retry. Default:Duration.from_seconds(1).max_delay(optional) ADuration. Default:Duration.from_minutes(5).jitter_strategy(optional) AJitterStrategyvalue. Default:JitterStrategy.FULL.retryable_errors(optional) List of strings or compiledre.Patternobjects matched against the error message. The SDK retries all errors when you set neitherretryable_errorsnorretryable_error_types.retryable_error_types(optional) List of exception classes. The SDK retries only exceptions that are instances of these classes. When you set both filters, the SDK retries an error if it matches either (OR logic).
RetryStrategy RetryStrategies.linearBackoff(
int maxAttempts,
Duration initialDelay,
Duration increment
)
RetryStrategy RetryStrategies.linearBackoff(
int maxAttempts,
Duration initialDelay,
Duration maxDelay,
Duration increment,
JitterStrategy jitter
)
Parameters:
maxAttemptsTotal attempts including the initial attempt.initialDelayAjava.time.Duration. Minimum 1 second.maxDelayAjava.time.Duration. Minimum 1 second. Caps the calculated delay.incrementAjava.time.Durationadded to the delay on each retry.jitterAJitterStrategyvalue. The three-argument overload omits bothmaxDelayandjitter.
The type is LinearRetryConfig.
type LinearRetryConfig struct {
// MaxAttempts is the maximum number of total attempts, including the
// first. The default is 6. It must not be negative.
MaxAttempts int
// InitialDelay is the delay before the first retry. The default is
// 1 second. When set, it must be at least 1 second.
InitialDelay time.Duration
// Increment is added to the delay before each retry after the first.
// The default is 1 second. It must not be negative.
Increment time.Duration
// MaxDelay caps the delay between retries. The default is 5 minutes.
// When set, it must be at least 1 second.
MaxDelay time.Duration
// Jitter is the jitter strategy applied to computed delays. The
// default is [JitterNone], so the default sequence is exact.
Jitter JitterStrategy
// RetryableErrors restricts retries to errors that at least one
// matcher reports as retryable. When empty, every error is retryable.
// Entries must not be nil. See [RetryConfig.RetryableErrors].
RetryableErrors []ErrorMatcher
// Has unexported fields.
}
Fields:
MaxAttemptsTotal attempts including the first. Default:6. Must not be negative. The zero value selects the default. UseNoRetry()orMaxAttempts: 1for a single attempt.InitialDelayAtime.Duration. Default:1 * time.Second. At least 1 second when set.IncrementAtime.Durationadded to the delay on each retry. Default:1 * time.Second. Must not be negative. A zero value selects the default, so this config cannot express a fixed interval. For a fixed interval, useNewRetryStrategywith aBackoffRateof 1.MaxDelayAtime.Duration. Default:5 * time.Minute. At least 1 second when set.JitterAJitterStrategyvalue. Default:JitterNone, so the default sequence is exact. When set, it must be one of the defined constants.RetryableErrorsA slice ofErrorMatcher. When empty, every error is retryable. Entries must not be nil.
The type is durable::builders::Backoff. Pass it to
RetryStrategyConfigBuilder::backoff. Set the other fields with the methods in
RetryStrategyConfig signature.
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum Backoff {
#[non_exhaustive]
Exponential {
rate: f64,
},
#[non_exhaustive]
Linear {
increment: Duration,
},
}
impl Backoff {
#[must_use]
pub fn exponential(rate: f64) -> Self;
// ...
#[must_use]
pub fn linear(increment: Duration) -> Self;
// ...
}
Constructors:
Backoff::exponential(rate)Multiplies the delay byrateon each retry.Backoff::linear(increment)Addsincrement, aDuration, to the delay on each retry. A zeroincrementgives a fixed interval.
Build a Backoff with these constructors. A match on a variant needs .., as in
Backoff::Linear { increment, .. }.
With a linear backoff, build() fills each unset field with the linear default.
max_attempts defaults to 6, initial_delay to 1 second, max_delay to 5
minutes, and jitter to JitterStrategy::Full.
There is no linear config type in .NET. Build the delay from RetryStrategy.FromDelegate:
// .NET ships no linear strategy. Build one with RetryStrategy.FromDelegate and
// compute a constant or linearly growing delay yourself.
public static IRetryStrategy RetryStrategy.FromDelegate(
Func<Exception, int, RetryDecision> strategy);
// The delegate returns a RetryDecision:
public static RetryDecision RetryDecision.RetryAfter(TimeSpan delay);
public static RetryDecision RetryDecision.DoNotRetry();
The delegate receives the failing exception and the 1-based attempt number and returns
a RetryDecision. Return RetryDecision.DoNotRetry() to stop, or
RetryDecision.RetryAfter(delay) to retry after a computed delay
(initialDelay + increment * (attempt - 1), capped at your own maxDelay).
Delay calculation¶
Linear backoff calculates the delay before each retry as:
base_delay = min(initial_delay + increment × (attempt - 1), max_delay)
final_delay = jitter(base_delay), minimum 1 second
The same JitterStrategy values apply: FULL, HALF, and NONE.
Write a custom strategy¶
You can write your own retry strategy directly. The SDK calls it with the error and the current attempt number after each failure. The attempt number is one-indexed.
RetryStrategy signature¶
import software.amazon.lambda.durable.retry.RetryDecision;
import software.amazon.lambda.durable.retry.RetryStrategy;
// @FunctionalInterface
// interface RetryStrategy {
// RetryDecision makeRetryDecision(Throwable error, int attempt);
// }
// attempt is one-indexed: 1 on the first retry, 2 on the second, etc.
A RetryStrategy is a function from a RetryAttempt to a RetryDecision. The
strategy receives one struct, with the error and the 1-based attempt number as
fields.
type RetryStrategy func(RetryAttempt) RetryDecision
type RetryAttempt struct {
// Err is the error from the attempt that just failed.
Err error
// Attempt is the 1-based number of the attempt that just failed,
// inclusive of the first attempt.
Attempt int
// Elapsed is the time since the first attempt began. It is zero when
// the SDK does not track it.
Elapsed time.Duration
// Has unexported fields.
}
type RetryDecision struct {
// Retry indicates whether the operation should be attempted again.
Retry bool
// Delay is how long to wait before the next attempt. It is ignored
// when Retry is false.
Delay time.Duration
// Has unexported fields.
}
A retry strategy is a function or closure that takes the &durable::StepError and
the 1-based attempt number, and returns a durable::RetryDecision. Pass it to the
step with .retry_strategy(..). StepError::source() returns the error that the
step body returned.
impl<O, F, Fut, S> StepBuilder<O, F, Fut, S>
where
O: Send + 'static,
F: FnOnce(StepContext) -> Fut + Clone + Send + 'static,
Fut: Future<Output = Result<O, BoxError>> + Send + 'static,
{
// ...
pub fn retry_strategy<R>(mut self, strategy: R) -> Self
where
R: Fn(&crate::StepError, u32) -> crate::RetryDecision + Send + Sync + 'static;
// ...
}
// ...
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RetryDecision {
#[non_exhaustive]
Retry {
delay: Duration,
},
Stop,
}
impl RetryDecision {
#[must_use]
pub fn retry(delay: Duration) -> Self;
// ...
}
public interface IRetryStrategy
{
// attempt is 1-based: 1 on the first retry, 2 on the second, etc.
RetryDecision ShouldRetry(Exception exception, int attemptNumber);
}
// Build one from a delegate without implementing the interface:
public static IRetryStrategy RetryStrategy.FromDelegate(
Func<Exception, int, RetryDecision> strategy);
Example¶
Return { shouldRetry: false } to stop, or
{ shouldRetry: true, delay: { seconds: N } } to retry.
import {
withDurableExecution,
StepConfig,
RetryDecision,
} from "@aws/durable-execution-sdk-js";
// A retry strategy is a plain function: (error: Error, attemptCount: number) => RetryDecision
// attemptCount is 1-based: 1 on the first retry, 2 on the second, etc.
const customRetryStrategy = (error: Error, attemptCount: number): RetryDecision => {
if (attemptCount >= 4) {
return { shouldRetry: false };
}
// Fixed 2-second delay regardless of attempt number
return { shouldRetry: true, delay: { seconds: 2 } };
};
const stepConfig: StepConfig<string> = { retryStrategy: customRetryStrategy };
export const handler = withDurableExecution(async (event, context) => {
const result = await context.step(
"call-api",
async () => callApi(),
stepConfig,
);
return result;
});
async function callApi(): Promise<string> {
return "ok";
}
Use RetryDecision.retry(Duration) or RetryDecision.no_retry().
from aws_durable_execution_sdk_python.config import Duration, StepConfig
from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python.retries import RetryDecision
# A retry strategy is a plain callable: (Exception, int) -> RetryDecision
# attempt_count is 1-based: 1 on the first retry, 2 on the second, etc.
def custom_retry_strategy(error: Exception, attempt_count: int) -> RetryDecision:
if attempt_count >= 4:
return RetryDecision.no_retry()
# Fixed 2-second delay regardless of attempt number
return RetryDecision.retry(Duration.from_seconds(2))
step_config = StepConfig(retry_strategy=custom_retry_strategy)
@durable_step
def call_api(step_context: StepContext) -> str:
return "ok"
@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> str:
return context.step(call_api(), config=step_config)
Use RetryDecision.retry(Duration) or RetryDecision.fail().
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.config.StepConfig;
import software.amazon.lambda.durable.retry.RetryDecision;
import software.amazon.lambda.durable.retry.RetryStrategy;
public class CustomRetryStrategyExample extends DurableHandler<Map<String, Object>, String> {
// RetryStrategy is a functional interface: (Throwable error, int attempt) -> RetryDecision
// attempt is 1-based: 1 on the first retry, 2 on the second, etc.
private static final RetryStrategy customRetryStrategy = (error, attempt) -> {
if (attempt >= 4) {
return RetryDecision.fail();
}
// Fixed 2-second delay regardless of attempt number
return RetryDecision.retry(Duration.ofSeconds(2));
};
@Override
public String handleRequest(Map<String, Object> event, DurableContext context) {
return context.step("call-api", String.class,
stepCtx -> callApi(),
StepConfig.builder().retryStrategy(customRetryStrategy).build());
}
private String callApi() {
return "ok";
}
}
Return RetryDecision{Retry: true, Delay: d} to retry, or RetryDecision{} to
stop. A zero delay waits one second. The SDK sends the delay in whole seconds and
rounds a fractional delay up. A negative delay fails the step.
package main
import (
"time"
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
// A RetryStrategy is a plain function: func(RetryAttempt) RetryDecision.
// Attempt is the 1-based number of the attempt that just failed.
func customRetryStrategy(a durable.RetryAttempt) durable.RetryDecision {
if a.Attempt >= 4 {
return durable.RetryDecision{}
}
// Fixed 2-second delay regardless of attempt number.
return durable.RetryDecision{Retry: true, Delay: 2 * time.Second}
}
func handler(ctx durable.Context, _ any) (string, error) {
return durable.Step(ctx, "call-api",
func(durable.StepContext) (string, error) {
return callAPI()
},
durable.WithRetry(customRetryStrategy),
)
}
func callAPI() (string, error) {
return "ok", nil
}
func main() { durable.Start(handler) }
Return RetryDecision::retry(delay) to retry, or RetryDecision::Stop to stop.
The SDK rounds a fractional delay up to whole seconds, and a delay under 1 second
waits 1 second.
use aws_durable_execution_sdk as durable;
use durable::RetryDecision;
use std::time::Duration;
fn custom_retry_strategy(_error: &durable::StepError, attempt: u32) -> RetryDecision {
if attempt >= 4 {
return RetryDecision::Stop;
}
RetryDecision::retry(Duration::from_secs(2))
}
async fn handler(
_event: serde_json::Value,
ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
let result = ctx
.step(|_| async { Ok(call_api()) })
.name("call-api")
.retry_strategy(custom_retry_strategy)
.await?;
Ok(result)
}
fn call_api() -> String {
"ok".to_owned()
}
#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
durable::init_default_subscriber()?;
durable::run(handler).await
}
Use RetryStrategy.FromDelegate((error, attempt) => ...). Return
RetryDecision.DoNotRetry() to stop, or RetryDecision.RetryAfter(delay) to retry.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
public class CustomRetryStrategyExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
// RetryStrategy.FromDelegate takes (Exception error, int attempt) => RetryDecision.
// attempt is 1-based: 1 on the first retry, 2 on the second, etc.
private static readonly IRetryStrategy CustomStrategy = RetryStrategy.FromDelegate((error, attempt) =>
{
if (attempt >= 4)
{
return RetryDecision.DoNotRetry();
}
// Fixed 2-second delay regardless of attempt number.
return RetryDecision.RetryAfter(TimeSpan.FromSeconds(2));
});
private async Task<string> Workflow(object input, IDurableContext ctx)
{
var config = new StepConfig { RetryStrategy = CustomStrategy };
return await ctx.StepAsync(
async (_, _) => CallApi(),
name: "call-api",
config: config);
}
private static string CallApi() => "ok";
}
Retry presets¶
The SDK ships with preset strategies for common cases:
import { withDurableExecution, retryPresets } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(async (event, context) => {
// Default: 6 attempts, 5s initial delay, 60s max, 2x backoff, full jitter
const result = await context.step(
"call-api",
async () => callApi(),
{ retryStrategy: retryPresets.default },
);
// Linear: 6 attempts, delays of 1s, 2s, 3s, 4s, 5s
const audit = await context.step(
"audit-log",
async () => writeAuditLog(),
{ retryStrategy: retryPresets.linear },
);
// No retry: fail immediately on first error
const critical = await context.step(
"charge-payment",
async () => chargePayment(),
{ retryStrategy: retryPresets.noRetry },
);
return { result, audit, critical };
});
async function callApi(): Promise<string> { return "ok"; }
async function writeAuditLog(): Promise<string> { return "logged"; }
async function chargePayment(): Promise<string> { return "charged"; }
retryPresets.default 6 attempts, 5s initial delay, 60s max, 2x backoff, full
jitter.
retryPresets.linear 6 attempts with linear delays of 1s, 2s, 3s, 4s, 5s and no
jitter.
retryPresets.noRetry 1 attempt, fails immediately on error.
from aws_durable_execution_sdk_python.config import Duration, StepConfig
from aws_durable_execution_sdk_python.retries import RetryPresets
# No retries
no_retry_config = StepConfig(retry_strategy=RetryPresets.none())
# Default retries (6 attempts, 5s initial delay, 60s max, 2x backoff)
default_config = StepConfig(retry_strategy=RetryPresets.default())
# Quick retries for transient errors (3 attempts)
transient_config = StepConfig(retry_strategy=RetryPresets.transient())
# Longer retries for resource availability (5 attempts, up to 5 minutes)
resource_config = StepConfig(retry_strategy=RetryPresets.resource_availability())
# Aggressive retries for critical operations (10 attempts)
critical_config = StepConfig(retry_strategy=RetryPresets.critical())
# Linear backoff (6 attempts, delays of 1s, 2s, 3s, 4s, 5s)
linear_config = StepConfig(retry_strategy=RetryPresets.linear())
# Fixed delay (5 attempts, 5 second interval). Pass an interval to customize.
fixed_config = StepConfig(retry_strategy=RetryPresets.fixed())
fixed_2s_config = StepConfig(
retry_strategy=RetryPresets.fixed(interval=Duration.from_seconds(2))
)
RetryPresets.default() 6 attempts, 5s initial delay, 60s max, 2x backoff, full
jitter.
RetryPresets.none() 1 attempt, fails immediately on error.
RetryPresets.transient() 3 attempts, 2x backoff, half jitter.
RetryPresets.resource_availability() 5 attempts, 5s initial delay, 5 min max, 2x
backoff.
RetryPresets.critical() 10 attempts, 1s initial delay, 60s max, 1.5x backoff, no
jitter.
RetryPresets.linear() 6 attempts with linear delays of 1s, 2s, 3s, 4s, 5s and no
jitter.
RetryPresets.fixed(interval) 5 attempts at a constant interval. Defaults to a
5 second interval. Pass a Duration to override.
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.retry.RetryStrategies;
public class RetryPresetsExample extends DurableHandler<Map<String, Object>, Map<String, Object>> {
@Override
public Map<String, Object> handleRequest(Map<String, Object> event, DurableContext context) {
// Default: 6 attempts, 5s initial delay, 60s max, 2x backoff, full jitter
String result = context.step("call-api", String.class,
stepCtx -> callApi(),
StepConfig.builder().retryStrategy(RetryStrategies.Presets.DEFAULT).build());
// Linear: 6 attempts, delays of 1s, 2s, 3s, 4s, 5s
String audit = context.step("audit-log", String.class,
stepCtx -> writeAuditLog(),
StepConfig.builder().retryStrategy(RetryStrategies.Presets.LINEAR).build());
// No retry: fail immediately on first error
String critical = context.step("charge-payment", String.class,
stepCtx -> chargePayment(),
StepConfig.builder().retryStrategy(RetryStrategies.Presets.NO_RETRY).build());
return Map.of("result", result, "audit", audit, "critical", critical);
}
private String callApi() { return "ok"; }
private String writeAuditLog() { return "logged"; }
private String chargePayment() { return "charged"; }
}
RetryStrategies.Presets.DEFAULT 6 attempts, 5s initial delay, 60s max, 2x backoff,
full jitter.
RetryStrategies.Presets.LINEAR 6 attempts with linear delays capped at 5 seconds
and no jitter.
RetryStrategies.Presets.NO_RETRY Fails immediately on first error.
package main
import (
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
type Results struct {
Result string `json:"result"`
Audit string `json:"audit"`
Critical string `json:"critical"`
}
func handler(ctx durable.Context, _ any) (Results, error) {
// Default: 6 attempts, 5s initial delay, 60s max, 2x backoff, full jitter.
result, err := durable.Step(ctx, "call-api",
func(durable.StepContext) (string, error) { return callAPI() },
durable.WithRetry(durable.ExponentialBackoff()))
if err != nil {
return Results{}, err
}
// Linear: 6 attempts, delays of 1s, 2s, 3s, 4s, 5s.
audit, err := durable.Step(ctx, "audit-log",
func(durable.StepContext) (string, error) { return writeAuditLog() },
durable.WithRetry(durable.MustLinearBackoff(durable.LinearRetryConfig{})))
if err != nil {
return Results{}, err
}
// No retry: fail immediately on first error.
critical, err := durable.Step(ctx, "charge-payment",
func(durable.StepContext) (string, error) { return chargePayment() },
durable.WithRetry(durable.NoRetry()))
if err != nil {
return Results{}, err
}
return Results{Result: result, Audit: audit, Critical: critical}, nil
}
func callAPI() (string, error) { return "ok", nil }
func writeAuditLog() (string, error) { return "logged", nil }
func chargePayment() (string, error) { return "charged", nil }
func main() { durable.Start(handler) }
ExponentialBackoff() 6 attempts, 5s initial delay, 60s max, 2x backoff, full
jitter. This is the default when you set no retry strategy.
NoRetry() 1 attempt, fails immediately on error.
MustLinearBackoff(LinearRetryConfig{}) 6 attempts with linear delays of 1s, 2s,
3s, 4s, 5s and no jitter.
The SDK ships no transient, critical, fixed, or resource-availability preset.
use aws_durable_execution_sdk as durable;
use durable::builders::RetryStrategyConfig;
use serde::Serialize;
#[derive(Serialize)]
struct Output {
result: String,
audit: String,
critical: String,
}
async fn handler(
_event: serde_json::Value,
ctx: durable::DurableContext,
) -> Result<Output, durable::BoxError> {
// Default: 6 attempts, 5s initial delay, 60s max, 2x backoff, full jitter
let result = ctx
.step(|_| async { Ok(call_api()) })
.name("call-api")
.retry_strategy_config(RetryStrategyConfig::default_preset())
.await?;
// Linear: 6 attempts, delays of 1s, 2s, 3s, 4s, 5s
let audit = ctx
.step(|_| async { Ok(write_audit_log()) })
.name("audit-log")
.retry_strategy_config(RetryStrategyConfig::linear())
.await?;
let critical = ctx
.step(|_| async { Ok(charge_payment()) })
.name("charge-payment")
.retry_strategy_config(RetryStrategyConfig::no_retry())
.await?;
Ok(Output {
result,
audit,
critical,
})
}
fn call_api() -> String {
"ok".to_owned()
}
fn write_audit_log() -> String {
"logged".to_owned()
}
fn charge_payment() -> String {
"charged".to_owned()
}
#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
durable::init_default_subscriber()?;
durable::run(handler).await
}
RetryStrategyConfig::default_preset() 6 attempts, 5s initial delay, 60s max,
2x backoff, full jitter. This is the default when you set no retry strategy.
RetryStrategyConfig::linear() 6 attempts with linear delays of 1s, 2s, 3s, 4s,
5s and no jitter.
RetryStrategyConfig::no_retry() 1 attempt, fails immediately on error.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
public class RetryPresetsExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, Results>(Workflow, input, context);
private async Task<Results> Workflow(object input, IDurableContext ctx)
{
// Default: 6 attempts, 5s initial delay, 60s max, 2x backoff, full jitter.
string result = await ctx.StepAsync(
async (_, _) => CallApi(),
name: "call-api",
config: new StepConfig { RetryStrategy = RetryStrategy.Default });
// Transient: 3 attempts, 1s initial delay, 5s max, 2x backoff, half jitter.
string audit = await ctx.StepAsync(
async (_, _) => WriteAuditLog(),
name: "audit-log",
config: new StepConfig { RetryStrategy = RetryStrategy.Transient });
// None: 1 attempt, fail immediately on first error.
string critical = await ctx.StepAsync(
async (_, _) => ChargePayment(),
name: "charge-payment",
config: new StepConfig { RetryStrategy = RetryStrategy.None });
return new Results(result, audit, critical);
}
private static string CallApi() => "ok";
private static string WriteAuditLog() => "logged";
private static string ChargePayment() => "charged";
}
public record Results(string Result, string Audit, string Critical);
RetryStrategy.Default 6 attempts, 5s initial delay, 60s max, 2x backoff, full
jitter.
RetryStrategy.Transient 3 attempts, 1s initial delay, 5s max, 2x backoff, half
jitter.
RetryStrategy.None 1 attempt, fails immediately on error.
Retry any durable operation¶
Use the withRetry helper to wrap any durable operation in a replay-safe retry loop.
The withRetry helper extends the same RetryStrategy configuration capability
available to step to other operations, such as invoke, waitForCallback, and
waitForCondition.
withRetry(context, name?, func, config) runs func and retries it on failure.
The function receives the durable context and the 1-based attempt number. By
default the loop is wrapped in runInChildContext so all attempts group under one
operation in execution history.
import {
withDurableExecution,
withRetry,
createRetryStrategy,
} from "@aws/durable-execution-sdk-js";
const retryStrategy = createRetryStrategy({
maxAttempts: 3,
initialDelay: { seconds: 2 },
backoffRate: 2,
});
export const handler = withDurableExecution(async (event, context) => {
// invoke does not accept a retryStrategy, so wrap it with withRetry to
// apply backoff between failed attempts.
const receipt = await withRetry(
context,
"charge-payment",
(ctx, attempt) =>
ctx.invoke<{ orderId: string }, string>(
`charge-${attempt}`,
"process-payment",
{ orderId: (event as { orderId: string }).orderId },
),
{ retryStrategy },
);
return receipt;
});
with_retry(context, func, config, name=None) runs func and retries it on
failure. The function receives the durable context and the 1-based attempt number.
By default the loop is wrapped in run_in_child_context so all attempts group
under one operation in execution history.
from aws_durable_execution_sdk_python.config import Duration
from aws_durable_execution_sdk_python.context import DurableContext
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python.retries import (
RetryStrategyConfig,
WithRetryConfig,
create_retry_strategy,
with_retry,
)
def charge_flow(ctx: DurableContext, attempt: int) -> str:
# invoke does not accept a retry strategy, so with_retry handles backoff.
return ctx.invoke(
function_name="process-payment",
payload={"order_id": "abc"},
name=f"charge-{attempt}",
)
retry_config = WithRetryConfig(
retry_strategy=create_retry_strategy(
RetryStrategyConfig(
max_attempts=3,
initial_delay=Duration.from_seconds(2),
backoff_rate=2.0,
)
),
)
@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> str:
return with_retry(
context,
func=charge_flow,
config=retry_config,
name="charge-payment",
)
DurableContext.withRetry(name, operation, config) runs operation and retries
it on failure. The BiFunction receives the 1-based attempt number first and the
durable context second. An async overload, withRetryAsync, returns a
DurableFuture<T> for parallel use.
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.config.WithRetryConfig;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.RetryStrategies;
public class WithRetryHelperExample extends DurableHandler<Map<String, Object>, String> {
@Override
public String handleRequest(Map<String, Object> event, DurableContext context) {
WithRetryConfig retryConfig = WithRetryConfig.builder()
.retryStrategy(RetryStrategies.exponentialBackoff(
3, // maxAttempts
Duration.ofSeconds(2), // initialDelay
Duration.ofMinutes(1), // maxDelay
2.0, // backoffRate
JitterStrategy.FULL)) // jitter
.build();
// invoke does not accept a retry strategy, so withRetry applies backoff
// between failed attempts.
return context.withRetry(
"charge-payment",
(attempt, ctx) -> ctx.invoke(
"charge-" + attempt,
"process-payment",
Map.of("orderId", event.get("orderId")),
String.class),
retryConfig);
}
}
Retry(ctx, name, fn, strategy, opts...) runs fn and retries it on failure. fn
receives a durable context and the 1-based attempt number, and may call any durable
operation. By default each attempt runs in its own child context named
<name>-attempt-<n>. Retry records no parent operation around the attempts.
Between attempts, Retry waits with a Wait named <name>-backoff-<n>.
package main
import (
"fmt"
"time"
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
type OrderEvent struct {
OrderID string `json:"orderId"`
}
type PaymentRequest struct {
OrderID string `json:"orderId"`
}
var retryStrategy = durable.MustNewRetryStrategy(durable.RetryConfig{
MaxAttempts: 3,
InitialDelay: 2 * time.Second,
BackoffRate: 2,
})
func handler(ctx durable.Context, event OrderEvent) (string, error) {
// Invoke does not accept a retry strategy, so wrap it with Retry to
// apply backoff between failed attempts.
return durable.Retry(ctx, "charge-payment",
func(c durable.Context, attempt int) (string, error) {
return durable.Invoke[string](c,
fmt.Sprintf("charge-%d", attempt),
"process-payment",
PaymentRequest{OrderID: event.OrderID},
)
},
retryStrategy,
)
}
func main() { durable.Start(handler) }
When the strategy stops, Retry returns a *RetryError. Its Attempts field holds
the number of attempts, and its Err field holds the final attempt's error. A zero
delay waits one second. A negative delay makes Retry return an error. A suspension
inside fn, such as an unfinished Wait, is not a failed attempt, and the strategy
never sees it. Retry returns an error for a nil fn, a nil strategy, or a
Context the SDK did not create.
WithAttemptChildContext(false) runs fn directly in the caller's context. The
strategy then receives the error fn returned. WithAttemptChildOptions(opts...)
passes ChildOption values, such as WithChildSerdes, to each attempt's child
context.
ctx.with_retry(f) runs f and retries it on failure. f receives a
DurableContext and the 1-based attempt number, and may call any durable
operation. The SDK calls f once per attempt, so clone each captured value inside
it. The strategy runs only after f returns an Err. A suspension inside f
leaves the current attempt in progress.
Set the strategy with .retry_strategy_config(..) or .retry_strategy(..).
Without one, the block uses RetryStrategyConfig::default_preset(). A
retry_if predicate in this block receives the attempt's durable::OperationError
as the source of its StepError. Downcast to that OperationError and match its
wire() error type or message. Do not downcast the recorded error to the attempt
body's live error, which is unavailable on replay.
use aws_durable_execution_sdk as durable;
use durable::builders::RetryStrategyConfig;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrderEvent {
order_id: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ChargeRequest {
order_id: String,
}
async fn handler(
event: OrderEvent,
ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
let retry_strategy = RetryStrategyConfig::builder()
.max_attempts(3)
.initial_delay(Duration::from_secs(2))
.backoff_rate(2.0)
.build();
// invoke takes no retry strategy, so wrap it with with_retry to apply
// backoff between failed attempts.
let order_id = event.order_id;
let receipt = ctx
.with_retry(move |child, attempt| {
let request = ChargeRequest {
order_id: order_id.clone(),
};
async move {
let receipt: String = child
.invoke("process-payment", request)
.name(format!("charge-{attempt}"))
.await?;
Ok(receipt)
}
})
.name("charge-payment")
.retry_strategy_config(retry_strategy)
.await?;
Ok(receipt)
}
#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
durable::init_default_subscriber()?;
durable::run(handler).await
}
By default the block records one child context with the name you set with
.name(..). Each attempt runs in its own child context, attempt-<n>, inside it.
Between attempts, a named block waits with a wait named <name>-backoff-<n>. A
block without a name creates waits without names. The invocation suspends during
this wait when all remaining SDK work is blocked.
When the strategy stops, the block fails with a durable::ChildContextError whose
source is the last attempt's error. .without_child_context() records no block.
The attempts then run in the caller's context as <name>-attempt-<n>, and the
block returns the last attempt's error unchanged.
.virtual_context() also records no block, but keeps the attempts and waits in a
separate operation namespace. The last call to .virtual_context() or
.without_child_context() selects the mode.
The .NET SDK has no separate withRetry helper, and InvokeConfig does not accept a
retry strategy. To retry another durable operation, wrap it in a step and set the
retry strategy on StepConfig.RetryStrategy. The step retries the wrapped operation
with backoff between failed attempts.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
public class WithRetryHelperExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<OrderEvent, string>(Workflow, input, context);
private async Task<string> Workflow(OrderEvent input, IDurableContext ctx)
{
// .NET has no separate withRetry helper. InvokeConfig does not accept a retry
// strategy, so wrap the invoke in a step and set the retry strategy on StepConfig.
// The step retries the invocation with backoff between failed attempts.
var config = new StepConfig
{
RetryStrategy = RetryStrategy.Exponential(
maxAttempts: 3,
initialDelay: TimeSpan.FromSeconds(2),
maxDelay: TimeSpan.FromMinutes(1),
backoffRate: 2.0,
jitter: JitterStrategy.Full),
};
return await ctx.StepAsync(
async (_, ct) => await ctx.InvokeAsync<PaymentRequest, string>(
"process-payment",
new PaymentRequest(input.OrderId),
name: "charge",
cancellationToken: ct),
name: "charge-payment",
config: config);
}
}
public record OrderEvent(string OrderId);
public record PaymentRequest(string OrderId);
The withRetry helper wraps the retry loop in a child context and uses context.wait
between attempts to suspend the invocation while waiting for the retry interval. The
child context, the wait operations, and any operations inside each attempt count toward
the durable operations the execution consumes. See
AWS Lambda service quotas.
Retry only specific errors¶
You can retry only certain error types and fail immediately on others.
Use retryableErrorTypes to specify which error classes to retry.
import {
withDurableExecution,
createRetryStrategy,
} from "@aws/durable-execution-sdk-js";
class RateLimitError extends Error {}
class ServiceUnavailableError extends Error {}
const retryStrategy = createRetryStrategy({
maxAttempts: 5,
initialDelay: { seconds: 2 },
// Only retry these specific error types; all other errors fail immediately
retryableErrorTypes: [RateLimitError, ServiceUnavailableError],
});
export const handler = withDurableExecution(async (event, context) => {
const result = await context.step(
"call-api",
async () => {
// Throws RateLimitError or ServiceUnavailableError on transient failures
return callApi();
},
{ retryStrategy },
);
return result;
});
async function callApi(): Promise<string> {
return "ok";
}
Use retryable_error_types to specify which exception classes to retry.
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
from aws_durable_execution_sdk_python.execution import durable_execution
from aws_durable_execution_sdk_python.retries import RetryStrategyConfig, create_retry_strategy
class RateLimitError(Exception):
pass
class ServiceUnavailableError(Exception):
pass
retry_strategy = create_retry_strategy(
RetryStrategyConfig(
max_attempts=5,
# Only retry these specific error types; all other errors fail immediately
retryable_error_types=[RateLimitError, ServiceUnavailableError],
)
)
step_config = StepConfig(retry_strategy=retry_strategy)
@durable_step
def call_api(step_context: StepContext) -> str:
# Raises RateLimitError or ServiceUnavailableError on transient failures
return "ok"
@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> str:
return context.step(call_api(), config=step_config)
RetryStrategy is a functional interface. Check the error type in the lambda and return
RetryDecision.fail() for errors you do not want to retry.
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.config.StepConfig;
import software.amazon.lambda.durable.retry.JitterStrategy;
import software.amazon.lambda.durable.retry.RetryDecision;
import software.amazon.lambda.durable.retry.RetryStrategies;
import software.amazon.lambda.durable.retry.RetryStrategy;
public class RetrySpecificErrorsExample extends DurableHandler<Map<String, Object>, String> {
static class RateLimitException extends RuntimeException {
public RateLimitException(String message) { super(message); }
}
static class ServiceUnavailableException extends RuntimeException {
public ServiceUnavailableException(String message) { super(message); }
}
// RetryStrategy is a functional interface: (Throwable error, int attempt) -> RetryDecision
// Filter by error type manually, then delegate to exponential backoff for the delay.
private static final RetryStrategy retryStrategy = (error, attempt) -> {
if (!(error instanceof RateLimitException) && !(error instanceof ServiceUnavailableException)) {
return RetryDecision.fail(); // all other errors fail immediately
}
return RetryStrategies.exponentialBackoff(5, Duration.ofSeconds(2), Duration.ofMinutes(1), 2.0, JitterStrategy.FULL)
.makeRetryDecision(error, attempt);
};
@Override
public String handleRequest(Map<String, Object> event, DurableContext context) {
return context.step("call-api", String.class,
stepCtx -> callApi(),
StepConfig.builder().retryStrategy(retryStrategy).build());
}
private String callApi() { return "ok"; }
}
Set RetryableErrors on the config to a list of ErrorMatcher values. The SDK
retries a failed attempt only when at least one matcher reports its error retryable.
Any other error fails the step on that attempt.
Build matchers with ErrorAs, ErrorIs, ErrorTypeIs, ErrorContains, or
ErrorMatches.
package main
import (
"time"
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
type RateLimitError struct{ Message string }
func (e *RateLimitError) Error() string { return e.Message }
type ServiceUnavailableError struct{ Message string }
func (e *ServiceUnavailableError) Error() string { return e.Message }
var retryStrategy = durable.MustNewRetryStrategy(durable.RetryConfig{
MaxAttempts: 5,
InitialDelay: 2 * time.Second,
// Only retry these specific error types; all other errors fail immediately.
RetryableErrors: []durable.ErrorMatcher{
durable.ErrorAs[*RateLimitError](),
durable.ErrorAs[*ServiceUnavailableError](),
},
})
func handler(ctx durable.Context, _ any) (string, error) {
return durable.Step(ctx, "call-api",
func(durable.StepContext) (string, error) {
// Returns *RateLimitError or *ServiceUnavailableError on transient failures.
return callAPI()
},
durable.WithRetry(retryStrategy),
)
}
func callAPI() (string, error) {
return "ok", nil
}
func main() { durable.Start(handler) }
ErrorAs and ErrorIs match a live error, such as the one a step returns. Inside a
default Retry group, match the recorded error with ErrorTypeIs, ErrorContains,
or ErrorMatches.
Pass a predicate to the builder's .retry_if(..). The SDK retries a failed attempt
only when the predicate returns true. Any other error fails the step on that
attempt. Read the step body's error with error.source(), which needs the
std::error::Error trait in scope. Match its type with is::<T>() or
downcast_ref::<T>(), or match its message with
error.source().map(ToString::to_string). The predicate must return the same answer
for the same error, because replay can call it again.
use aws_durable_execution_sdk as durable;
use durable::builders::RetryStrategyConfig;
use std::error::Error as _;
use std::time::Duration;
#[derive(Debug)]
struct RateLimitError;
impl std::fmt::Display for RateLimitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("rate limited")
}
}
impl std::error::Error for RateLimitError {}
#[derive(Debug)]
struct ServiceUnavailableError;
impl std::fmt::Display for ServiceUnavailableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("service unavailable")
}
}
impl std::error::Error for ServiceUnavailableError {}
async fn handler(
_event: serde_json::Value,
ctx: durable::DurableContext,
) -> Result<String, durable::BoxError> {
let retry_strategy = RetryStrategyConfig::builder()
.max_attempts(5)
.initial_delay(Duration::from_secs(2))
// All other errors fail immediately.
.retry_if(|error| {
error.source().is_some_and(|source| {
source.is::<RateLimitError>() || source.is::<ServiceUnavailableError>()
})
})
.build();
let result = ctx
.step(|_| async { Ok(call_api()) })
.name("call-api")
.retry_strategy_config(retry_strategy)
.await?;
Ok(result)
}
fn call_api() -> String {
"ok".to_owned()
}
#[tokio::main]
async fn main() -> Result<(), lambda_runtime::Error> {
durable::init_default_subscriber()?;
durable::run(handler).await
}
Pass retryableExceptions to RetryStrategy.Exponential(...) to retry only those
exception types (and their subclasses). Every other exception fails immediately.
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
public class RetrySpecificErrorsExample
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<object, string>(Workflow, input, context);
private async Task<string> Workflow(object input, IDurableContext ctx)
{
// Pass retryableExceptions to retry only these exception types (and subclasses).
// Every other exception fails immediately.
var config = new StepConfig
{
RetryStrategy = RetryStrategy.Exponential(
maxAttempts: 5,
initialDelay: TimeSpan.FromSeconds(2),
maxDelay: TimeSpan.FromMinutes(1),
backoffRate: 2.0,
jitter: JitterStrategy.Full,
retryableExceptions: new[]
{
typeof(RateLimitException),
typeof(ServiceUnavailableException),
}),
};
return await ctx.StepAsync(
async (_, _) => CallApi(),
name: "call-api",
config: config);
}
private static string CallApi() => "ok";
}
public class RateLimitException : Exception
{
public RateLimitException(string message) : base(message) { }
}
public class ServiceUnavailableException : Exception
{
public ServiceUnavailableException(string message) : base(message) { }
}