Durable functions¶
AWS Lambda durable functions let a single handler orchestrate long-running workflows (up to a year) with checkpointed steps and compute-free waits: on every resume the code replays from the top and completed operations return their stored results instead of re-executing.
sosw integrates this natively: sosw.durable.get_durable_lambda_handler is the
durable twin of sosw.app.get_lambda_handler() — the exact same Processor lifecycle and
warm start behavior, wrapped with the durable_execution decorator of the
AWS Durable Execution SDK.
pip install sosw[durable]
Important
Every Python version supported by
sosw(3.12 – 3.14) satisfies the durable SDK’s own minimum Python requirement.The SDK is an optional dependency: plain
import sosw/import sosw.appnever import it, so regular functions pay zero overhead. Onlyget_durable_lambda_handlerrequires it and raises a helpfulImportErrormentioningpip install sosw[durable]when it is missing.The managed AWS Lambda Python runtimes already bundle the SDK, so a deployed function can run with an empty
requirements.txt— but AWS recommends pinning the SDK in production deployment packages.
Writing a durable Processor¶
Inside the Processor, global_vars.lambda_context is the DurableContext of the current
invocation: use its step() / wait() operations for checkpointed work.
from sosw.app import LambdaGlobals, Processor as SoswProcessor
from sosw.durable import get_durable_lambda_handler, durable_step, Duration
class Processor(SoswProcessor):
def __call__(self, event, **kwargs):
super().__call__(event)
# Checkpointed step: on replay the stored result is returned, the body is skipped.
data = global_vars.lambda_context.step(self.fetch_data(key=event['key']))
# Compute-free wait: the execution suspends, you are not billed for the pause.
global_vars.lambda_context.wait(Duration(seconds=300))
return global_vars.lambda_context.step(self.publish_report(data=data))
@durable_step
def fetch_data(step_context, self, key):
step_context.logger.info("Fetching %s", key)
return {'key': key, 'rows': 42}
@durable_step
def publish_report(step_context, self, data):
return {'published': data['key']}
global_vars = LambdaGlobals()
lambda_handler = get_durable_lambda_handler(Processor, global_vars)
Warning
Note the parameter order of @durable_step methods: (step_context, self, ...).
Accessing self.fetch_data binds self as the first call argument of the decorator
wrapper, and at execution time the SDK prepends the live StepContext — so the original
function receives (step_context, self, *args). This is the least obvious part of the
integration; plain functions decorated with @durable_step receive just
(step_context, *args) as in the official SDK examples.
Enabling durability in SAM¶
Durability is per-function configuration — DurableConfig on AWS::Serverless::Function
(or AWS::Lambda::Function):
MyDurableFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: my-durable-function
CodeUri: src/
Handler: app.lambda_handler # returned by get_durable_lambda_handler
Runtime: python3.14
Timeout: 900 # per-invocation compute timeout
MemorySize: 512
DurableConfig:
ExecutionTimeout: 259200 # whole durable execution, seconds (up to 1 year)
RetentionPeriodInDays: 1 # checkpoint / history retention
DeadLetterConfig: # recommended: failed async executions are NOT retried
Type: SQS
TargetArn: !GetAtt MyDlq.Arn
ExecutionTimeout bounds the whole durable execution (across all replays and waits) and is
independent of the regular Timeout, which still bounds each compute slice.
Invoking durable functions¶
Durable functions must be invoked with a qualified ARN (a version, an alias, or $LATEST):
response = boto3.client('lambda').invoke(
FunctionName=f'arn:aws:lambda:{region}:{account}:function:my-durable-function:$LATEST',
Payload=json.dumps(payload))
An unqualified name fails with InvalidParameterValueException. Pin real version numbers or
aliases in production so that in-flight executions replay against the code that started them.
A synchronous invoke() returns not your handler result, but an envelope:
{"Status": "SUCCEEDED" | "FAILED" | "PENDING", "Result": "<JSON string>", ...} (PENDING
when the execution outlives the synchronous call). Unwrap it with
sosw.durable.parse_durable_result — pure Python, no SDK required on the caller side:
from sosw.durable import parse_durable_result
payload = json.loads(response['Payload'].read())
result = parse_durable_result(payload) # raises RuntimeError on FAILED / PENDING / empty
One codebase, durable and not¶
sosw.durable.durable_wait(seconds) waits through the checkpointed wait() of the
DurableContext when running durable, and falls back to time.sleep() otherwise — so the same
Processor code can ship as both a durable and a regular Lambda:
from sosw.durable import durable_wait
durable_wait(30) # DurableContext.wait(Duration(seconds=30)) or time.sleep(30)
The fallback path works without the SDK installed.
Operational constraints¶
Durable execution is checkpoint/replay. The rules below come from the AWS programming model and from production experience; violating them produces bugs that only show up on replays.
- Code outside steps must be deterministic.
On every resume the whole handler re-runs from the top; only durable operations return stored results. Never drive control flow outside steps with
random,time.time(),uuidor other non-deterministic inputs — or replays will diverge from the original run.- Step payloads: JSON-serializable, under 256 KB.
Step arguments and return values must be JSONifiable. Results under 256 KB are checkpointed directly; keep step payloads small and pass references (S3 keys, table keys) instead of blobs. A durable execution result over the limit may come back empty through a synchronous invoke —
parse_durable_resultraisesRuntimeErrorfor that case.- Instance attributes are not replayed.
Only step arguments and returns are reliably restored. The Processor is additionally a warm-container singleton shared across executions — keep per-execution workflow state in step results or external stores (DynamoDB), never on
self.- Never place a durable step after a variable-length wait loop.
The SDK identifies operations by their slot in the execution sequence. A polling loop that calls
wait()a non-deterministic number of iterations (e.g. zero on the terminal replay) shifts the slot counter, and astep()placed after the loop can land on a slot recorded as a wait — the SDK returns the cached wait result (None) and silently skips the step body. Run post-loop finalizers inline (idempotent, untracked), or use the SDK’swait_for_conditioninstead of hand-rolled polling.- Version carefully.
New code versions must tolerate checkpoints written by old ones: do not rename or reorder steps of in-flight executions. Deploy new versions/aliases and let old executions drain.
- Configure a DLQ for async invocations.
Failed asynchronous durable executions are not retried automatically — attach a
DeadLetterConfigand/or EventBridge rules on the FAILED / STOPPED / TIMED_OUT states.
Note
Durable functions are the in-process successor of the self-hosted task queue that the 0.7.x
line of sosw shipped — see the migration guide.
API reference¶
View Licence Agreement- sosw.durable.get_durable_lambda_handler(processor_class, global_vars=None, custom_config=None, **durable_kwargs)[source]¶
Durable twin of
sosw.app.get_lambda_handler().Builds exactly the same warm-start caching
lambda_handler(single Processor instance cached inglobal_varsfor the lifetime of the Lambda container) and wraps it with thedurable_executiondecorator of the AWS Lambda Durable Execution SDK. Inside the Processor,global_vars.lambda_contextis theDurableContextof the current invocation: use itsstep()/wait()operations for checkpointed durable operations.Requires the optional durable SDK:
pip install sosw[durable]. Regular functions should keep usingsosw.app.get_lambda_handler(), which does not even import the SDK.- Parameters:
processor_class – Callable processor class.
global_vars – Lambda’s global variables (processor, context).
custom_config – Custom configuration to pass the processor constructor.
durable_kwargs – Optional keyword arguments to pass through to the
durable_executiondecorator of the SDK.
- Returns:
Function reference for the lambda handler.
- sosw.durable.durable_wait(seconds: int, global_vars=None)[source]¶
Wait either using the checkpointed
wait()of the Durable Execution context (when running as a durable function), or falling back to a regulartime.sleep()otherwise.This helper lets the very same Processor code run both as a durable and as a regular Lambda. The fallback path does not require the durable SDK to be installed.
- Parameters:
seconds (int) – Number of seconds to wait.
global_vars – Optional instance of
LambdaGlobalsholding thelambda_context. Defaults to the module-levelsosw.app.global_vars.
- sosw.durable.parse_durable_result(payload)[source]¶
Unwrap the invocation envelope returned by a synchronous boto3
invoke()of a durable function:{"Status": "SUCCEEDED" | "FAILED" | "PENDING", "Result": <JSON string>, ...}.The business output of the durable handler is JSON-encoded inside
Resultwhen theStatusisSUCCEEDED. Payloads that are not durable envelopes are returned unchanged, so the callers can treat durable and regular Lambdas uniformly. This helper is pure Python for the CALLER side: the durable SDK is not required.- Parameters:
payload – Parsed JSON payload of the Lambda invocation response.
- Returns:
The unwrapped result of the durable execution (or the original payload).
- Raises:
RuntimeError – If the durable execution FAILED, is still PENDING, or the
Resultis empty or not valid JSON.