A common moment in AWS projects goes like this:
You have a Lambda function that receives a request, does some work, and then you realize there’s a second piece of work that also needs to happen. So the obvious thought is:
“I’ll just call another Lambda from this Lambda.”
It works. It feels clean. It’s “microservices-ish”.
However, in most real-world systems, direct Lambda-to-Lambda calling becomes a trap: it is harder to scale, harder to debug, and more prone to breaking in production.
Let’s talk about why, and the better patterns you can use instead (SQS, EventBridge, Step Functions, SNS, and a few more).
What “Lambda calling Lambda” usually looks like
There are two common versions:
1) Synchronous call (wait for the result)
Lambda A invokes Lambda B and waits.
- A can’t finish until B finishes.
- A “owns” B’s latency and failures.
2) Asynchronous call (fire-and-forget)
Lambda A invokes Lambda B “Event” style (async).
- A finishes quickly.
- But you still created a direct dependency between functions.
Both can be valid in some cases. But most teams regret making it their default.
Why direct Lambda → Lambda calls are usually a bad idea
1) You create tight coupling
If Lambda A “knows” Lambda B’s name, payload structure, versioning rules, IAM permissions, and error behavior… that’s coupling.
Now changes in B can break A.
And soon you end up with:
- fragile deployments
- “Don’t touch that function,” fear
- hidden dependencies nobody remembers
2) You inherit failure and retry problems (the nasty kind)
Retries in distributed systems are rarely simple.
Example:
- Lambda A calls Lambda B
- B times out or fails
- A retries
- B actually succeeded the first time, but the response didn’t return
- Now the action runs twice (duplicate charge, duplicate email, duplicate record…)
Without strong idempotency, you will eventually create duplicates.
3) You stack timeouts and latency
Lambda has a max runtime, and your API or upstream service may have its own timeout too.
If A waits for B, you’re stacking latency:
- cold starts
- network call time
- B runtime
- Plus A runtime
Users feel that delay, and your error rate rises under load.
4) Scaling can get weird (and expensive)
When Lambda A triggers Lambda B directly, you can accidentally create burst amplification.
If A scales to 1,000 concurrent executions and each calls B, suddenly:
- B gets slammed instantly
- downstream services (DB, external API) get slammed next
- throttling starts
- retries multiply the load
Queues and orchestration patterns exist mainly to protect your system from itself.
5) Observability becomes painful
When the workflow is spread across multiple functions:
- tracing across A → B becomes essential
- logs are split
- debugging is “jump between CloudWatch log groups.”
You can solve this (X-Ray, structured logs, correlation IDs), but direct invocation makes it easier to build a system that’s hard to reason about.
6) The recursion/loop risk is real
It’s surprisingly easy to create loops:
A calls B
B calls C
C calls A (or triggers A via some event)
Now you’ve built an infinite money-burning machine.
The better patterns (what to use instead)
Option 1: SQS queue (best default for background work)

If Lambda A needs to “hand off” work to be processed later, SQS is usually the cleanest solution.
Why it’s great
- decouples producer and consumer
- smooths traffic spikes
- built-in retries + DLQ support
- you can control batch size and concurrency
- easy to scale safely
Flow
- Lambda A sends a message to SQS
- Lambda B is triggered by SQS
- B processes it; failures retry automatically
Mini example (Node.js) — send to SQS
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
const sqs = new SQSClient({});
export async function handler(event) {
const payload = { userId: event.userId, action: "GENERATE_REPORT" };
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.QUEUE_URL,
MessageBody: JSON.stringify(payload),
}));
return { ok: true };
}
Use SQS when:
- the second task can be async
- you want reliability and buffering
- you want a clean separation between “request handling” and “processing.”
Option 2: EventBridge (best for event-driven architecture)

If what you’re doing is more like:
“Something happened, and multiple parts of the system may care.”
Then EventBridge is the best tool.
Why it’s great
- The producer doesn’t know the consumers
- multiple targets can subscribe (Lambdas, Step Functions, SQS, etc.)
- you can filter events with rules
- You can evolve over time without rewriting producers
Flow
- Lambda A publishes an event like
OrderCreated - EventBridge routes it to Lambda B / Lambda C / etc.
Mini example (Node.js) — publish EventBridge event
import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge";
const eb = new EventBridgeClient({});
export async function handler(event) {
await eb.send(new PutEventsCommand({
Entries: [{
Source: "app.orders",
DetailType: "OrderCreated",
Detail: JSON.stringify({ orderId: event.orderId }),
EventBusName: process.env.EVENT_BUS_NAME,
}]
}));
return { ok: true };
}
Use EventBridge when:
- you want loose coupling
- you expect multiple consumers
- you want a clean “domain event” style architecture
Option 3: Step Functions (best for workflows and orchestration)

If you truly need:
- a multi-step flow
- branching logic
- retries per-step
- waiting for external callbacks
- auditing and visibility
Then Step Functions is the right choice.
Instead of Lambda A manually calling B, C, D… you let a state machine coordinate them.
Why it’s great
- built-in retries and error handling
- visual workflow
- much easier debugging
- supports parallel steps
- avoids “glue code” inside Lambda
Use Step Functions when:
- it’s a real workflow, not just “one extra task”
- you want control and observability
- you’re tired of debugging chained Lambdas
Option 4: SNS (best for fan-out notifications)

SNS is simple and useful when you want to broadcast a message.
Common flow
- publish to SNS topic
- multiple subscribers receive it (Lambda, SQS, HTTP endpoints)
SNS is great for:
- sending notifications
- lightweight fan-out
- integrating with SQS for durable processing (SNS → SQS → Lambda)
Option 5: Direct async Lambda invoke (sometimes acceptable)

AWS lets you invoke Lambda asynchronously using the SDK.
This is better than sync chaining, but still couples services (A must know B).
Use it when:
- you’re doing something small
- you control both functions
- you’ve decided coupling is acceptable
- you still implement idempotency and good logging
It’s not “wrong”. It’s just not the best default.
Option 6: DynamoDB Streams / S3 Events / Kinesis (event sources)

Sometimes you don’t need “A calls B” at all.
Instead, the handoff happens naturally via data/events:
- S3 event triggers a processing Lambda when an object is uploaded
- DynamoDB Streams triggers a Lambda when a record changes
- Kinesis triggers a Lambda for stream processing
This is often cleaner because:
- you stop thinking in “function calls”
- you start thinking in “state changes” and “events”
Quick decision guide: what should you pick?
- Need reliable background processing? → SQS
- Need publish/subscribe events across services? → EventBridge
- Need a multi-step workflow with retries/visibility? → Step Functions
- Need simple fan-out notifications? → SNS
- Need stream processing? → Kinesis
- Need trigger-on-change? → DynamoDB Streams / S3 events
When is Lambda → Lambda direct calling actually okay?
There are valid cases. For example:
- You need a shared internal capability (like “GenerateThumbnail”) and accept coupling
- The downstream function is stable, versioned, and treated like an internal API
- You’re using it as a temporary step while migrating to a better design
- You’re doing async invoke, and the workflow is simple
If you do it, add guardrails:
- use idempotency keys
- add correlation IDs to logs
- define clear timeouts
- handle partial failures
- add DLQ or failure notification paths
A practical example of architecture
Let’s say you have an API request:
User clicks “Export Report.”
Bad default:
- API Lambda calls Report Lambda synchronously
- The request hangs for 20–40 seconds
- timeouts and retries create duplicates
Better:
- API Lambda stores an “export requested” record (DB)
- API Lambda sends a job to SQS
- Worker Lambda processes export
- Worker uploads result to S3
- The worker publishes the
ReportExportedevent via EventBridge - Notification Lambda sends email / in-app notification
Now your system is:
- resilient
- scalable
- observable
- easier to modify
Final thought
Direct Lambda → Lambda calls feel like function calls in code, but production cloud systems don’t behave like a single codebase.
Queues and event buses exist to make distributed systems survivable.
If you remember one thing from this article, make it this:
When you feel like calling a Lambda from another Lambda, pause and ask:
“Is this a workflow (Step Functions), a job (SQS), or an event (EventBridge)?”




