Lambda vs Step Functions vs EventBridge: Compute, Orchestrate, Route
9 min readBytePatterns
Lambda vs Step Functions vs EventBridge: one runs a function per event, one orders steps with retries and waits, one routes events by content to targets.
Lambda vs Step Functions vs EventBridge in one line: Lambda runs your code when something happens, Step Functions decides which steps run in which order, with retries, branches and waits, and EventBridge decides which of those things should hear about an event at all. They are three layers, not three alternatives: EventBridge routes, Step Functions orchestrates, Lambda computes. The question in a design is which layer a given problem belongs to. Facts come from the AWS documentation listed at the end, as of October 2026.
| Service | What it does | Its unit |
|---|---|---|
| Lambda | runs a function in response to an event | an invocation, at most 15 minutes |
| Step Functions | runs steps as a state machine: sequence, choice, retry, wait | an execution, up to a year for Standard |
| EventBridge | matches events against rules and sends them to targets | an event on a bus, matched by content |
The problem it solves
An order is placed. Stock must be reserved, the card charged, and the parcel shipped; the charge fails now and then and should be retried; gold-tier orders also go to a manual review queue; and finance wants to hear about refunds without the orders team changing code for them. Written as one Lambda function, this becomes a function that sleeps, retries, branches and knows every consumer's address, and that dies at the fifteen-minute mark if the gateway is slow. Each of those jobs has a service built for it.
How a single function scales and what a cold start costs is in AWS Lambda cold starts. This article is about what sits around the function.
The intuition
Lambda is compute. You upload code, pick a runtime and memory size, and Lambda runs it in execution environments it creates on demand, one invocation at a time per environment, scaling by adding environments. An invocation lasts at most 15 minutes. The function is where business logic lives, and nowhere in it should there be a loop that waits for something external for a long time, because you pay for the wait and hit the limit.
Step Functions is orchestration. A state machine, written in Amazon States Language as JSON, describes steps and the order between them: Task states that call a Lambda function or many AWS service APIs directly, Choice for branching, Wait for pauses, Parallel and Map for fan-out, and Retry and Catch fields that handle errors per state without any code. The machine keeps the state between steps, so each Lambda function can be short and stateless. Standard workflows run for up to a year, run each execution exactly once, and are priced per state transition; Express workflows run for up to five minutes, at high rate, with at-least-once semantics for asynchronous runs, and are priced per execution and duration. A human approval that takes three days is a Wait or a task token in a Standard workflow, not a Lambda function.
EventBridge is routing. Producers put events on an event bus, with a source, a detail-type and a detail payload. Rules carry an event pattern, a JSON document naming the fields and the values that match, and each matching rule forwards the event to its targets: a Lambda function, a Step Functions state machine, an SQS queue, an API destination, another bus. The default bus already receives events from AWS services. Producers do not know who listens, so finance can add a rule for OrderRefunded without touching the orders service. EventBridge Scheduler fires events on a schedule, Pipes connects one source to one target with filtering and enrichment, and an archive lets you replay past events. How EventBridge compares with a queue and a topic is in SQS vs SNS vs EventBridge.
Put together, the order flow reads: EventBridge rule for OrderPlaced starts a Step Functions execution; the machine calls a reserve-stock function, a charge-card function with three retries, and a ship function; a second rule sends gold orders to a review queue; a third sends refunds to finance. Each piece does one job.
Watch it run
The lesson animation is the Lambda layer under load. Upload events arrive through an SQS queue; the first batch finds no environment, so Lambda builds one, the cold start. A burst brings more cold starts and a concurrency of three, reserved concurrency caps it there, and the rest of the burst waits in the queue. A failing message ends in the dead-letter queue. All of that is the compute scaling itself. What the animation does not show is who put the events there and what should happen after the function returns; that is EventBridge in front and Step Functions around it.
Lambda & Event-Driven Design
Step 1 of 11
Upload events land in a queue. Lambda runs a poller that reads the queue for you and invokes your function with batches.
The same interactive animation as the lesson — step through it with the controls.
The code
The state machine for the order, illustrative, in Amazon States Language. The retry on the charge step is configuration, not code:
{"StartAt": "ReserveStock",
"States": {
"ReserveStock": {"Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:reserve",
"Next": "ChargeCard"},
"ChargeCard": {"Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:charge",
"Retry": [{"ErrorEquals": ["States.TaskFailed"], "MaxAttempts": 3, "IntervalSeconds": 2}],
"Next": "Ship"},
"Ship": {"Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789012:function:ship",
"End": true}}}
A toy model of the three layers, not of any AWS API. First the router: an EventBridge-like pattern matcher, where every field in the pattern must take one of the listed values, nested objects included:
def matches(pattern, event):
"""Toy model of an EventBridge event pattern: every listed field must match one value."""
for key, allowed in pattern.items():
value = event.get(key)
if isinstance(allowed, dict):
if not isinstance(value, dict) or not matches(allowed, value):
return False
elif value not in allowed:
return False
return True
RULES = [
{"name": "orders-to-fulfilment", "pattern": {"source": ["shop.orders"], "detail-type": ["OrderPlaced"]},
"target": "states:FulfilOrder"},
{"name": "refunds-to-finance", "pattern": {"source": ["shop.orders"], "detail-type": ["OrderRefunded"]},
"target": "lambda:NotifyFinance"},
{"name": "big-orders-to-review", "pattern": {"source": ["shop.orders"], "detail": {"tier": ["gold"]}},
"target": "sqs:ManualReview"},
]
def route(event):
return [r["target"] for r in RULES if matches(r["pattern"], event)]
order = {"source": "shop.orders", "detail-type": "OrderPlaced", "detail": {"orderId": "o-77", "tier": "gold"}}
print("targets:", route(order))
print("targets:", route({"source": "shop.orders", "detail-type": "OrderRefunded", "detail": {"tier": "basic"}}))
print("targets:", route({"source": "shop.inventory", "detail-type": "StockLow", "detail": {}}))
# targets: ['states:FulfilOrder', 'sqs:ManualReview']
# targets: ['lambda:NotifyFinance']
# targets: []
The gold order matched two rules and went to two targets; the producer named neither. The inventory event matched nothing and was dropped, which is what a bus does with an event nobody subscribed to.
Next the orchestrator and the compute. Three Lambda-like handlers, one of which fails twice before it works, and a tiny Standard-workflow runner that walks the machine and applies Retry itself:
attempts = {"ChargeCard": 0}
def charge_card(state): # a Lambda-like handler: fails twice, then works
attempts["ChargeCard"] += 1
if attempts["ChargeCard"] < 3:
raise RuntimeError("payment gateway timeout")
return dict(state, charged=True)
def reserve_stock(state):
return dict(state, reserved=True)
def ship(state):
return dict(state, shipped=True)
MACHINE = {
"StartAt": "ReserveStock",
"States": {
"ReserveStock": {"Type": "Task", "Resource": reserve_stock, "Next": "ChargeCard"},
"ChargeCard": {"Type": "Task", "Resource": charge_card, "Retry": {"MaxAttempts": 3}, "Next": "Ship"},
"Ship": {"Type": "Task", "Resource": ship, "End": True},
},
}
def run(machine, state):
"""Toy model of a Standard workflow: one state at a time, Retry handled by the machine."""
name, path = machine["StartAt"], []
while True:
spec = machine["States"][name]
for attempt in range(1, spec.get("Retry", {}).get("MaxAttempts", 0) + 2):
try:
state = spec["Resource"](state)
path.append(f"{name}" + (f" (attempt {attempt})" if attempt > 1 else ""))
break
except RuntimeError as e:
path.append(f"{name} failed: {e}")
else:
return path + ["ExecutionFailed"], state
if spec.get("End"):
return path + ["ExecutionSucceeded"], state
name = spec["Next"]
path, final = run(MACHINE, order["detail"])
for step in path:
print(" ", step)
print(final)
# ReserveStock
# ChargeCard failed: payment gateway timeout
# ChargeCard failed: payment gateway timeout
# ChargeCard (attempt 3)
# Ship
# ExecutionSucceeded
# {'orderId': 'o-77', 'tier': 'gold', 'reserved': True, 'charged': True, 'shipped': True}
The charge handler contains no retry loop and no knowledge of what runs before or after it; the machine retried it and carried the state from step to step. Had the real gateway taken an hour to recover, a Standard workflow would have waited it out with IntervalSeconds and BackoffRate, while a single Lambda function would have died at fifteen minutes.
The complexity
The three bill in different units, which is often what decides the design:
- Lambda bills per request and per gigabyte-second of duration. Time spent waiting inside a function is paid for.
- Step Functions Standard bills per state transition, so a machine with many small steps costs more than one with few; Express bills per execution and duration, which suits short, high-volume flows.
- EventBridge bills per event published to a custom bus; events from AWS services on the default bus are not charged for rule matching.
Latency also differs. A direct Lambda invocation is the fastest path; a Standard workflow adds a transition between every step; EventBridge adds a routing hop before any of it, in exchange for producers and consumers not knowing each other.
Where it goes wrong
- Waiting inside Lambda. Polling for a payment result or sleeping until a human approves burns duration and hits the 15-minute limit. Make it a
Waitstate or a task token in Step Functions. - Orchestrating from a function. One Lambda function that calls the others in order becomes the state store, the retry logic and the timeout, all at once. That is the state machine's job.
- Hard-coding consumers. A function that knows finance's queue has to change when finance changes. Publish the event and let a rule carry it.
- Choosing Standard for a hot path. Thousands of short executions a second are Express territory; Standard's per-transition price and exactly-once guarantees are for long, important flows.
- A pattern that matches nothing. Event patterns are exact on values and case; an event with
detail-typeoforderPlacedwill not match a rule forOrderPlaced, and the event is silently dropped.
When it shows up in interviews
As the "walk me through an order" design, where the interviewer listens for the layering: an event on a bus, a workflow that owns the sequence and retries, and functions that each do one step. The follow-up is usually a long wait, approval or a slow third party, and the strong answer moves it out of compute into the workflow. Certification questions pair the services with their words: compute, orchestration, routing.
How to say it in an interview
"Lambda is the compute: it runs a function per event for up to fifteen minutes and scales by adding environments. Step Functions is the orchestration: a state machine that sequences those functions, branches, retries with backoff and waits for hours or days without any code running, with Standard workflows for long exactly-once flows and Express for short high-volume ones. EventBridge is the routing: producers publish events to a bus and rules match them by content to targets, so consumers come and go without the producer changing. For the order flow I'd publish OrderPlaced to EventBridge, start a Step Functions execution from a rule, and keep each step a small Lambda function."
The compute choice under this, when a container or an instance beats a function, is in Lambda vs Fargate vs EC2, and the queue in front of the function is the Lambda and event-driven design lesson.
Frequently asked questions
What is the difference between Lambda and Step Functions?
Lambda runs one function in response to an event, for at most 15 minutes, and holds no state between invocations. Step Functions runs a state machine that calls functions and other services in order, keeps the state between steps, retries and branches by configuration, and can wait or run for up to a year in a Standard workflow.
When should I use EventBridge instead of Step Functions?
Use EventBridge when the question is who should receive an event: it matches events by content and fans them out to targets that the producer does not know. Use Step Functions when the question is what happens next, in order: sequencing, retries, waits and branches for one flow. Often a rule starts a workflow.
What is the difference between Standard and Express workflows in Step Functions?
Standard workflows run for up to a year, execute each workflow exactly once, keep full execution history and are priced per state transition; they suit long-running, auditable flows. Express workflows run for up to five minutes at high rates, with at-least-once semantics when started asynchronously, and are priced per execution and duration.
Sources
- What is AWS Lambda? — AWS Lambda Developer Guide
- Lambda quotas — AWS Lambda Developer Guide
- What is Step Functions? — AWS Step Functions Developer Guide
- Choosing workflow type in Step Functions — AWS Step Functions Developer Guide
- Handling errors in Step Functions workflows — AWS Step Functions Developer Guide
- What is Amazon EventBridge? — Amazon EventBridge User Guide
- Rules in Amazon EventBridge — Amazon EventBridge User Guide
- Amazon EventBridge event patterns — Amazon EventBridge User Guide
- Amazon EventBridge Pipes — Amazon EventBridge User Guide