Updated recently
Last updated:
Quick Answer: AWS Lambda runs your code, EventBridge routes events between systems on content-based patterns, and Step Functions coordinates multi-step workflows with retries, branching and state. Use Lambda for single jobs, EventBridge when several independent services must react to the same thing happening, and Step Functions when a process has an order, a timeout, or a human approval step.
AWS Lambda Step Functions EventBridge: Which Piece Does What
The three services get bundled together constantly, and the confusion is understandable — they overlap. Here is the clean split, with the decision test I actually use in design reviews.
| Service | What It Does | Use It When | Do Not Use It When |
|---|---|---|---|
| Lambda | Runs a function in response to an event. Stateless, per-invocation billing. | One job with a clear input and output. | The job needs ordering guarantees or runs longer than 15 minutes. |
| EventBridge | Matches event patterns and delivers them to targets. No state, no ordering. | A fan-out: many services reacting to the same event, or SaaS/app integration. | You need to sequence steps or track where something is in a process. |
| Step Functions | Orchestrates state machines: sequencing, branching, retries, waits, parallelism. | Multi-step business flows with failure handling and audit history. | A single call. Wrapping one Lambda in a state machine is ceremony. |
The rule of thumb: EventBridge answers “who else cares that this happened”, Step Functions answers “what happens next”. Teams that skip this distinction end up either chaining twenty Lambda functions through ad-hoc self-invocations — an untestable mess with no execution history — or writing a 900-line state machine for what is really a single HTTP call.
Why EventBridge Beats Direct Triggers Once You Have More Than Three Services
With one producer and one consumer, a direct trigger is simpler and you should use it. The moment a second and third team want to react to the same event, direct wiring becomes a fan-out problem you solve by editing the producer’s code — which means every new consumer is a deploy to a service that has nothing to do with them.
EventBridge inverts that. The producer emits an event with a source and a detail type. Each consumer adds a rule with a content pattern. Nobody edits anybody else’s code. The rule below matches only orders above a threshold from one source, which is exactly the sort of filter that, in a direct-trigger world, would end up as an if statement in a Lambda that should never have known about orders.
A second practical benefit: schema registry and archives. EventBridge can capture schemas from live traffic and archive events for replay, which is quietly one of the most useful features in the serverless stack when you are debugging a flow that failed three days ago.
One operational point teams learn the hard way: rules, permissions and targets are all infrastructure. Manage them with the same tooling as everything else rather than clicking them into existence — our IaC comparison covers which tool fits a serverless-heavy estate best.
Building a Real Order Flow
The example below is a simplified version of something close to production: an order arrives, it is validated, stock is reserved, payment is checked after a short wait, and then it ships. Failures at any point get flagged for review rather than silently dropped.
- API Gateway receives the order and writes it, then puts
Order Placedon EventBridge. - An EventBridge rule matches that event and invokes
reserve-stockLambda. - That Lambda starts a Step Functions execution — this is the orchestration boundary.
- The state machine calls
validate-order, waits for the payment gateway’s asynchronous capture, then callsship-order. - Anything unrecoverable lands in an SQS dead-letter queue with the full execution payload attached.
The State Machine Definition
This is AWS States Language, the JSON that Step Functions actually executes. Note the Retry blocks — retries belong in the state machine, not scattered through business code, because that is where you can see and audit them.
{
"Comment": "Order fulfilment flow",
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-south-1:123456789012:function:validate-order",
"Retry": [
{"ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException"],
"IntervalSeconds": 2, "MaxAttempts": 3, "BackoffRate": 2.0}
],
"Catch": [{"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "FlagForReview"}],
"Next": "ReserveStock"
},
"ReserveStock": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {"FunctionName": "reserve-stock", "Payload.$": "$.order"},
"Retry": [{"ErrorEquals": ["States.ALL"], "IntervalSeconds": 5, "MaxAttempts": 2}],
"Next": "AwaitPayment"
},
"AwaitPayment": {
"Type": "Wait",
"Seconds": 15,
"Next": "CheckPayment"
},
"CheckPayment": {
"Type": "Choice",
"Choices": [
{"Variable": "$.payment.status", "StringEquals": "CAPTURED", "Next": "ShipOrder"}
],
"Default": "FlagForReview"
},
"ShipOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:ap-south-1:123456789012:function:ship-order",
"End": true
},
"FlagForReview": {
"Type": "Task",
"Resource": "arn:aws:states:::sqs:sendMessage",
"Parameters": {
"QueueUrl": "https://sqs.ap-south-1.amazonaws.com/123456789012/fulfilment-dlq",
"MessageBody.$": "$"
},
"End": true
}
}
}
Two details worth copying. ResultPath: "$.error" in the Catch keeps the original input alongside the error rather than overwriting it, so whoever investigates gets both. And the Choice state’s Default branch means an unknown payment status is a reviewable failure, not a silent success.
Wiring the Rule and the Function
The CLI version of the same setup — this is the part you should be able to do without a console.
# 1. Package and create the consumer function
aws lambda create-function
--function-name reserve-stock
--runtime python3.12
--handler app.handler
--role arn:aws:iam::123456789012:role/lambda-basic-execution
--zip-file fileb://reserve-stock.zip
--environment "Variables={TABLE_NAME=orders,THRESHOLD=500}"
# 2. Let EventBridge invoke it — without this the rule silently does nothing
aws lambda add-permission
--function-name reserve-stock
--statement-id eventbridge-invoke
--action lambda:InvokeFunction
--principal events.amazonaws.com
--source-arn arn:aws:events:ap-south-1:123456789012:rule/orders-placed
# 3. Content-based rule
aws events put-rule
--name orders-placed
--event-pattern '{"source":["myapp.orders"],"detail-type":["Order Placed"]}'
aws events put-targets --rule orders-placed
--targets Id=1,Arn=arn:aws:lambda:ap-south-1:123456789012:function:reserve-stock
# 4. Create and start the state machine
aws stepfunctions create-state-machine
--name order-fulfilment
--definition file://statemachine.json
--role-arn arn:aws:iam::123456789012:role/stepfunctions-execution
Step 2 is where most first attempts fail. The permission is a resource policy on the function itself; without it you get an AccessDeniedException in CloudWatch Events that says nothing useful about what you forgot.
Idempotency: The Part Everyone Skips
A real incident: the day twenty-one customers were charged twice
A payments team built exactly this flow, correctly, and it worked for four months. Then their card processor started taking longer than usual to respond. Lambda’s own timeout fired at three seconds, the client retried, and the capture step ran twice for the same order. Nothing in the state machine checked whether a capture had already happened, because the first version of the code never needed it.
Twenty-one customers were double-charged over a festival weekend. The fix was not complex — an idempotency key derived from the order ID, stored in DynamoDB with a conditional write, so the second attempt found the key and returned the original result. The interesting part is what it teaches about event-driven design: at-least-once delivery is the default everywhere in AWS. EventBridge, SQS, Lambda retries and Step Functions retries all assume you might see the same event more than once. Any step with a financial or externally-visible side effect must be safe to run twice.
In my experience this is the single most common production defect in serverless systems, and it is nearly invisible in testing because duplicate delivery is rare until it is not.
Retries, Dead Letters and What Failure Actually Looks Like
Design retries deliberately rather than accepting defaults. Three layers, each with a job:
- Step Functions Retry — for transient downstream failures, with exponential backoff and a bounded attempt count.
- Lambda’s async destination or an on-failure destination — for events that never made it into a workflow.
- SQS dead-letter queue — the landing zone for anything exhausted, with an alarm attached. A DLQ nobody reads is decoration.
Also decide your error taxonomy up front. Retrying a validation error is pointless — it will fail identically forever. Retrying a throttled API call is correct. Distinguishing the two is most of the practical work in making an event-driven system behave.
One operational note: give every execution a correlation ID that appears in every log line and every event payload. When an order goes missing across four services, the absence of that ID turns a two-hour investigation into a five-minute one.
The Bill Nobody Modelled
Serverless cost conversations usually start with the Lambda price and miss the actual driver. In a workflow-heavy system, Step Functions Standard transitions and EventBridge rule evaluations dominate, along with CloudWatch Logs — which at high invocation volume frequently costs more than the compute.
The practical levers: use Express workflows for high-volume, short-lived tasks; keep Lambda log levels at INFO or WARN in production rather than DEBUG; batch where your domain allows it; and set a billing alarm per environment. A small monthly surprise in a dev account is a configuration problem, not a billing error.
Also watch the Lambda provisioned concurrency line if you added it to fight cold starts — it bills while idle, and forgotten dev environments accumulate it quickly.
If your functions run inside a VPC, add another line to the model: NAT gateway hours and data processing. The usual fix — gateway endpoints for S3 and DynamoDB instead of routing through NAT — is explained properly in our AWS VPC guide, and it is often the largest single saving in a serverless account.
For a sense of where this pattern is heading in AI systems, event-driven orchestration around model calls is exactly how production RAG pipelines are assembled — see our Bedrock RAG walkthrough for the same building blocks in an inference context.
Training Resources
Build this end to end in SkilBrill AWS Application Development Training — Lambda, API Gateway, Step Functions and EventBridge taught as one working system, with idempotency, retries and DLQs covered as core material rather than afterthoughts.
FAQ
What is the difference between AWS Lambda, Step Functions and EventBridge?
Lambda executes a single function. EventBridge routes events to interested subscribers using content-based rules. Step Functions orchestrates multi-step workflows with ordering, branching, waits and auditable retries. EventBridge answers “who cares”, Step Functions answers “what happens next”.
When should I use EventBridge instead of a direct Lambda trigger?
As soon as more than one service needs to react to the same event, or when producers and consumers should not be coupled in code. Direct triggers are simpler for one-to-one flows; EventBridge wins once fan-out begins.
Do Step Functions and Lambda guarantee exactly-once processing?
No. AWS offers at-least-once delivery across EventBridge, Lambda asynchronous invocation and Step Functions retries. Anything with a financial or external side effect needs an idempotency key — typically a conditional DynamoDB write.
Is Step Functions expensive for high-volume workflows?
It can be. Standard workflows bill per state transition, and chatty state machines accumulate transitions fast. Use Express workflows for short, high-volume tasks, and count transitions before designing a 12-state flow that could be three.
How do I handle failures in an event-driven AWS architecture?
Layer them: bounded retries with backoff in the state machine, an on-failure destination for Lambda, and an SQS dead-letter queue for everything exhausted — each with an alarm. Classify retryable errors separately from permanent ones so you never retry a validation failure.
