Serverless Orchestration: Step Functions vs Temporal
Coordinate multi-step serverless workflows with Step Functions, Temporal, or Durable Functions — state, retries, and error handling handled.
Overview
A single AWS Lambda function can process an HTTP request, resize an image, or validate a form. But real-world workflows are rarely that simple. An e-commerce order involves validating inventory, charging payment, sending confirmation, updating analytics, and scheduling shipment. Each step is a function; the workflow is the coordination logic that decides what to call next, what to do on failure, and how to retry transient errors. That coordination layer — a managed state machine that invokes your functions in order — is serverless orchestration.
Writing this coordination inside Lambda functions creates spaghetti code: function A calls B, which calls C, which calls D. If D fails, C must handle compensation, which may also fail, requiring more error handling. The business logic becomes entangled with networking, retries, timeouts, and state persistence. Workflow orchestration tools — AWS Step Functions, Azure Durable Functions, Temporal — externalize this coordination into state machines. Functions become pure business logic; the orchestrator handles execution flow, state, retries, and error recovery. The examples below cover state machine design, orchestrator trade-offs, and a production-ready order workflow on all three platforms.
When to use it
Use this recipe when:
- A serverless process has more than two dependent steps. See Event-Driven Functions for building individual function units.
- Steps must be retried with backoff on transient failures. See Retry Logic for exponential backoff patterns.
- Workflow state must survive function timeouts or crashes.
- Human approval or external event waits are required mid-workflow.
- Several functions must run in parallel branches with join points. For an AWS-only deep dive with map states and batch processing, see the Step Functions workflow recipe.
Skip the orchestrator when a single function or a simple synchronous call chain does the job: a state machine adds per-transition cost and one more system to operate.
Solution
The order-processing workflow below is identical across all three implementations: validate the order, check inventory, charge payment, fan out to email and analytics, then schedule shipment. Runnable copies of all three live in the companion repository.
AWS Step Functions (ASL, Amazon States Language)
{
"Comment": "Order Processing Workflow",
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:validateOrder",
"Next": "CheckInventory",
"Catch": [
{
"ErrorEquals": ["ValidationException"],
"Next": "NotifyCustomerInvalid"
}
]
},
"CheckInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:checkInventory",
"Next": "ProcessPayment",
"Retry": [
{
"ErrorEquals": ["ServiceUnavailable"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2
}
]
},
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:processPayment",
"Next": "ParallelTasks",
"Catch": [
{
"ErrorEquals": ["PaymentFailed"],
"Next": "ReleaseInventory"
}
]
},
"ParallelTasks": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "SendEmail",
"States": {
"SendEmail": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:sendEmail",
"End": true
}
}
},
{
"StartAt": "UpdateAnalytics",
"States": {
"UpdateAnalytics": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:updateAnalytics",
"End": true
}
}
}
],
"Next": "ScheduleShipment"
},
"ScheduleShipment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:scheduleShipment",
"End": true
},
"ReleaseInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:releaseInventory",
"Next": "NotifyCustomerFailed"
},
"NotifyCustomerInvalid": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:notifyCustomer",
"End": true
},
"NotifyCustomerFailed": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:notifyCustomer",
"End": true
}
}
}
Temporal Workflow (TypeScript SDK)
import { workflow, activity } from '@temporalio/workflow';
const validateOrder = activity('validateOrder');
const checkInventory = activity('checkInventory');
const processPayment = activity('processPayment');
const sendEmail = activity('sendEmail');
const updateAnalytics = activity('updateAnalytics');
const scheduleShipment = activity('scheduleShipment');
const releaseInventory = activity('releaseInventory');
export async function orderWorkflow(order: OrderInput): Promise<OrderResult> {
try {
await validateOrder(order);
await checkInventory(order.items);
await processPayment({ orderId: order.id, amount: order.total });
// Parallel execution
await Promise.all([
sendEmail({ orderId: order.id, template: 'confirmation' }),
updateAnalytics({ event: 'order_placed', orderId: order.id }),
]);
await scheduleShipment({ orderId: order.id, address: order.address });
return { status: 'completed', orderId: order.id };
} catch (error) {
if (error.type === 'PaymentFailed') {
await releaseInventory({ orderId: order.id });
}
throw error;
}
}
Azure Durable Functions (Python)
import azure.functions as func
import azure.durable_functions as df
myApp = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@myApp.route(route="orchestrators/{functionName}")
@myApp.durable_client_input(client_name="client")
async def http_start(req: func.HttpRequest, client):
instance_id = await client.start_new(req.route_params["functionName"], None, None)
return client.create_check_status_response(req, instance_id)
@myApp.orchestration_trigger(context_name="context")
def order_orchestrator(context: df.DurableOrchestrationContext):
order = context.get_input()
try:
yield context.call_activity("validate_order", order)
yield context.call_activity("check_inventory", order["items"])
yield context.call_activity("process_payment", order)
# Fan-out/fan-in for parallel tasks
tasks = [
context.call_activity("send_email", order),
context.call_activity("update_analytics", order),
]
yield context.task_all(tasks)
yield context.call_activity("schedule_shipment", order)
return {"status": "completed"}
except Exception as e:
if "PaymentFailed" in str(e):
yield context.call_activity("release_inventory", order)
raise
Explanation
- State machines: Step Functions models workflows as JSON state machines. Each state is a step (Task, Choice, Parallel, Wait, Pass), and transitions define the flow. AWS persists the state machine itself, so if a Lambda times out, Step Functions retries it under the configured policy without losing workflow context.
- Durable execution (Temporal): Temporal workflows are code, not JSON. A workflow function runs as a deterministic event replay; every activity execution is recorded as an event. If the worker crashes, another worker replays the workflow from the last recorded event, skipping already-completed activities. This makes workflows durable without explicit state management.
- Fan-out / fan-in: parallel branches (sending email and updating analytics simultaneously) reduce total workflow duration. In Step Functions, use a
Parallelstate. In Temporal, usePromise.all(). In Durable Functions, usetask.all(). The orchestrator waits for all branches to complete before continuing. - External events: workflows can wait for external signals. A workflow may pause at “Wait for manual approval” for hours or days. Step Functions supports callbacks and wait-for-callback tasks. Temporal supports signals — external messages that a workflow can await. Durable Functions supports external events via
wait_for_external_event().
Variants
| Orchestrator | Format | Vendor | Best for | Cost model |
|---|---|---|---|---|
| Step Functions | JSON ASL | AWS | AWS-native, visual design | Per state transition |
| Temporal | Code (any language) | Open-source | Complex logic, portability | Self-hosted / Cloud |
| Durable Functions | Code (.NET/JS/Python) | Azure | Azure Functions users | Execution + storage |
| Camunda | BPMN | Open-source | Business process modeling | Self-hosted / SaaS |
What Works
- Keep Lambda functions stateless and idempotent: the orchestrator handles state; functions should be pure. If a function is retried, it must produce the same result without side effects.
- Use exponential backoff with jitter for retries: retrying every 1 second creates thundering herds. Add random jitter to spread retries. Step Functions supports this natively; in Temporal, configure retry policies on activities.
- Set timeouts on every activity: an unbounded wait for an external API can stall a workflow forever. Set per-activity timeouts (e. g. , 30 seconds for payment processing, 5 minutes for human approval). Distinguish between retryable (timeout, 5xx) and non-retryable (4xx, validation) errors.
- Store workflow input/output in S3 for large payloads: Step Functions has a 256KB payload limit. If your workflow passes large files or datasets, store them in S3 and pass URIs through the state machine. Temporal has a 2MB gRPC limit; use blob storage similarly for large data.
- Monitor workflow execution metrics: track success rate, average duration, retry count per activity, and state transition cost.
Common mistakes
- Putting business logic in the state machine: Step Functions JSON is for coordination, not business rules. Complex conditionals and transformations belong in Lambda functions. A state machine with dozens of
Choicestates is unmaintainable — refactor logic into the functions. - Passing secrets through workflow state: workflow state is logged and visible in the orchestrator console. Never pass API keys, tokens, or PII in state payloads. Pass references (e. g. , order ID) and let functions retrieve secrets from AWS Secrets Manager or Azure Key Vault.
- Ignoring idempotency on activity retries: Step Functions may retry a Lambda that partially succeeded (e. g. , charged the customer but timed out before returning). The retry charges the customer again. Always design activities to check idempotency keys before mutating state.
- Not versioning state machines: changing a live state machine definition affects in-flight executions.
Advanced: Saga Pattern with Compensation
For workflows that span several services (order, inventory, payment, shipping), the saga pattern coordinates distributed transactions with compensating actions on failure:
// Temporal saga with compensation
export async function orderSaga(workflowId: string, order: Order): Promise<void> {
const compensations: (() => Promise<void>)[] = [];
try {
await Activities.reserveInventory(order);
compensations.push(() => Activities.releaseInventory(order));
await Activities.chargePayment(order);
compensations.push(() => Activities.refundPayment(order));
await Activities.scheduleShipment(order);
// No compensation for shipment — once shipped, it's done
} catch (err) {
// Run compensations in reverse order
for (const compensate of compensations.reverse()) {
try { await compensate(); }
catch (c) { console.error('Compensation failed:', c); }
}
throw err;
}
}
Each step registers a compensation before moving to the next. If any step fails, compensations run in reverse order. This pattern works with all three orchestrators: Step Functions uses Catch blocks with compensation Lambda invocations, Durable Functions uses try/catch with activity calls. For a dedicated walkthrough with rollback workflows and idempotency, see the Saga pattern recipe.
Troubleshooting
- Execution stuck in a Running state: an activity is hanging or its heartbeat timed out. Check the failed state in the Step Functions console (or
temporal workflow describeoutput) then verify the activity’s timeout and heartbeat interval. - State transition retries spike: a downstream dependency is flaky. Check retry counts per state in CloudWatch; a hot retry loop usually means a missing idempotency key on a write operation.
- Payload exceeds the 256KB limit: the execution fails with
States.DataLimitExceeded. Store the payload in S3 and pass{ "bucket": ..., "key": ... }through the state machine instead. - State machine update breaks in-flight executions: long-running executions keep the definition they started with. Publish changes as a new version behind an alias and shift traffic gradually.
- Express workflow hits the 5-minute cap: Express executions stop at 5 minutes. Move long-running work to a Standard workflow or split it into chained executions.
Further Reading
- AWS Step Functions documentation — state types, quotas, and Express vs Standard pricing.
- Temporal documentation — durable execution, signals, and workflow versioning.
- Azure Durable Functions documentation — orchestrator patterns and durable timers.
- Step Functions workflow recipe — AWS-only deep dive with map states and batch processing.
Production Notes
- Ship state machine changes as versions: in-flight executions keep the definition they started with, so move the alias only after a canary percentage passes.
- Alert on workflow-level metrics: failed executions, p99 execution duration, and throttled state transitions; Lambda error alerts alone miss stuck workflows.
- Log the execution ARN in every activity: a single log line should trace a Lambda invocation back to its workflow execution ID.
- Test compensation paths in staging: run the failure branches quarterly, not just the happy path; they are the ones that run when things go wrong for real.
Key Takeaways
- Reach for an orchestrator once a workflow has more than two dependent steps or needs retries, waits, or human approval.
- Step Functions fits AWS-native workflows with visual monitoring; Temporal fits complex logic and multi-cloud; Durable Functions fits Azure Functions shops.
- Keep activities stateless and idempotent; the orchestrator owns state, so every retry must be safe.
- Pass references, not payloads: workflow state is logged and Step Functions caps input at 256KB.
Common Production Pitfalls
- Editing a live state machine instead of publishing a new version; in-flight executions silently keep running the old definition.
- Setting activity timeouts longer than the task heartbeat window, so healthy work gets killed as unresponsive.
- Passing full payloads through states and hitting the 256KB limit in production; it never happened in dev because the test orders were small.
- Retrying a non-idempotent charge or write activity and billing the customer twice.
- Choosing Express for long-running work (5-minute cap) or Standard for high-volume short jobs and paying per state transition.
- Only testing the happy path; compensation branches sit untested until the day they run for real.
Frequently Asked Questions
Should I use Step Functions or Temporal?
Step Functions is simpler for AWS-centric workflows with visual monitoring. Temporal is more capable for complex logic, cross-cloud portability, and long-running workflows with arbitrary code. If you need to run workflows on-premise or across clouds, Temporal is the better choice.
How do I handle long waits (hours or days) in a workflow?
Use wait states with callbacks. Step Functions supports Wait states with timestamps. Temporal workflows can sleep for days without the worker actively running during the sleep; the orchestrator schedules a wake-up event. Durable Functions supports durable timers.
What is the cost of Step Functions?
Standard workflows charge per state transition. A 10-state workflow executed 1,000,000 times costs ~$25. Express workflows charge per execution and duration, optimized for high-volume, short-duration workflows (sub-5 minutes). Choose Express for high throughput, Standard for long-running workflows.
Can I test workflows locally?
Step Functions Local (Docker) runs state machines locally. Temporal ships a local dev server (temporal server start-dev). Durable Functions has a local runtime integrated with Azure Functions Core Tools. Always test workflows locally before deploying.
How do I implement the saga pattern with compensation?
Register a compensation function before each step in the workflow. If any step fails, run compensations in reverse order. In Temporal, use a compensations array and iterate in reverse on catch. In Step Functions, use Catch blocks that invoke compensation Lambda functions. Each compensation should be idempotent — it may run twice.
How do I version workflows without breaking in-flight executions?
In Step Functions, use versions and aliases to deploy new definitions without affecting running executions. In Temporal, use WorkflowVersion to branch logic based on the version the execution started with. In Durable Functions, use orchestration versioning with IDurableOrchestrationContext. Always test new versions with a canary percentage before full rollout.
What is the maximum workflow duration?
Step Functions Standard: up to 1 year per execution. Express: up to 5 minutes. Temporal: no hard limit; workflows can run indefinitely. Durable Functions: up to the storage retention period (default 7 days, configurable). For workflows longer than the platform limit, use checkpoints and restart the workflow from the last checkpoint.
Related Resources
Minimize Cold Start Latency in Serverless Functions
How to reduce cold start times in AWS Lambda, Azure Functions, and Cloud Run using provisioned concurrency, lazy loading, runtime tuning, and dependency optimization.
RecipeDesign Event-Driven Systems with Event Buses and Brokers
How to design loosely coupled services with events, event buses, and message brokers — pub/sub patterns and event sourcing for async communication.
RecipeManage Distributed Transactions with the Saga Pattern
How to implement saga orchestration and choreography to maintain data consistency across microservices without distributed transactions or two-phase commit.
RecipeDesign a Scalable API Gateway for Microservices
Build an API gateway that routes requests, handles authentication, rate limiting, caching, and protocol translation between clients and backend microservices.
RecipeImplement Event Sourcing in Serverless Architectures
How to capture all changes as immutable events using event sourcing with AWS Lambda, DynamoDB streams, and event stores for audit trails and temporal queries.
RecipeOrchestrate Serverless Workflows with AWS Step Functions
Build state machine workflows with AWS Step Functions using sequential, parallel, and map states for orchestrating Lambda functions and long-running processes.