
When Should You Stop Adding Lambda Functions to an AWS Architecture?
Learn when to stop adding AWS Lambda functions, spot architecture warning signs and choose Step Functions, Fargate or native AWS services for a workload.
AWS Lambda has a habit of becoming the answer to problems you did not originally plan to solve with Lambda.
It starts reasonably. An API needs a small piece of backend logic, so you add a function. An S3 upload needs processing, so another function appears. Then you need a scheduled job, an SQS consumer, a webhook handler, a data transformation, and a few steps connecting AWS services.
A few months later, the architecture looks something like this:
API Gateway ----> Lambda
|
S3 -------------> Lambda
|
SQS ------------> Lambda
|
EventBridge -----> Lambda
|
DynamoDB Stream -> Lambda
|
Step Functions --> Lambda --> Lambda --> Lambda
There is nothing automatically wrong with this diagram. Lambda is designed for event-driven workloads, and using many functions can be a perfectly sensible architecture.
The problem starts when a team stops asking why each function exists.
At some point, another Lambda function may no longer be the simplest answer. It may be compensating for a workflow problem, acting as plumbing between AWS services, carrying a workload that wants to be a container, or splitting one application into pieces that are becoming harder to operate than the original system.
So when should you stop adding Lambda functions?
I would start by asking a different question:
Does this new function contain useful computation, or is it repairing an architecture that no longer fits the workload?
More Lambda Functions Are Not Automatically a Problem
Function count is a poor architecture metric.
An application with 80 small functions can be easier to maintain than an application with eight functions if each one has a clear responsibility, independent deployment boundary, understandable event source, and predictable failure behavior.
Consider an image-processing system:
S3 Upload
|
v
Lambda: Validate
|
v
EventBridge
|
+----> Lambda: Generate Thumbnail
|
+----> Lambda: Extract Metadata
|
+----> Lambda: Moderate Image
Each function responds to a clear event and performs bounded work. The functions can scale independently, failures can be isolated, and adding another image-processing consumer does not necessarily disturb the others.
That is a natural Lambda architecture. The warning sign is not the number of boxes. It is when you struggle to explain why a box needs to be a Lambda function at all.
Signal 1: Lambda Is Only Moving Data Between AWS Services
Imagine an SQS message arrives and a Lambda function does this:
def handler(event, context):
for record in event["Records"]:
payload = transform(record["body"])
start_workflow(payload)
Sometimes that transformation contains real business logic and Lambda is exactly where it belongs.
Other times the function exists only because the original developer needed to connect Service A to Service B. It parses a small payload, changes a field name, and immediately calls another AWS service.
The architecture becomes:
SQS
|
v
Lambda
|
v
Step Functions
Before adding that function, check whether the services can communicate through a native AWS capability. EventBridge Pipes, Step Functions service connections, EventBridge targets, API destinations, and direct service connections can remove some glue code entirely.
The difference matters because even a tiny Lambda function creates things the team now owns:
- Source code
- IAM permissions
- Deployment configuration
- Runtime updates
- Logging
- Error handling
- Monitoring
- Tests
- Dependency maintenance
Removing unnecessary compute does not just remove a box from an architecture diagram. It removes something that can fail.
This is also why I prefer thinking about serverless architecture as a collection of managed capabilities, rather than treating serverless as another word for Lambda. A serverless application can use queues, event buses, workflow services, databases, APIs, and functions together without requiring custom compute at every connection.
Signal 2: You Are Building a Workflow Inside Function Code
A more interesting warning sign appears when Lambda starts coordinating other Lambda functions.
Consider an order process:
Lambda: Create Order
|
v
Lambda: Reserve Inventory
|
v
Lambda: Charge Payment
|
v
Lambda: Arrange Shipment
|
v
Lambda: Send Confirmation
There is nothing technically impossible about one function invoking another. The problem is that workflow state, retries, branching, and recovery can gradually become hidden inside application code.
What happens if payment succeeds but shipment creation fails? Should the payment step run again? Where do you record which steps have already completed? How long should the process wait for an external response? What happens if an operation requires manual approval?
The code often starts growing its own workflow engine:
if inventory_reserved:
try_payment()
if payment_failed:
release_inventory()
if payment_pending:
wait_and_retry()
if payment_succeeded:
create_shipment()
At that point, the architecture problem is not "How do I invoke the next Lambda?"
It is "How do I represent the state of this business process?"
This is where an orchestrator can become more useful than another function. AWS Step Functions workflow patterns are designed for coordinating tasks, retries, branches, waits, and service interactions while keeping the execution state visible outside individual functions.
Lambda can still perform the business logic:
Plain text
Step Functions|\+------------+------------+| | |v v vLambda Lambda LambdaReserve Payment Shipping
The difference is that Lambda performs work while the workflow service owns the sequence. That boundary usually becomes easier to reason about as the process grows.
Signal 3: Your Function Wants to Be a Long-Running Process
For years, one of the easiest rules for Lambda architecture was based on execution duration. If work could not complete within the Lambda execution window, another compute model deserved consideration.
That rule is less simple now.
Standard Lambda functions are still designed around bounded invocations, while newer AWS capabilities give developers more choices for long-running work. Durable Functions can maintain state across long-running workflows, and some Lambda deployment models can support workloads beyond the traditional standard-function execution pattern.
So duration alone should not decide the architecture. Instead, look at the shape of the workload.
Suppose a process needs to:
- Run continuously.
- Maintain an active connection.
- Host a persistent application server.
- Execute a background worker indefinitely.
- Keep expensive resources loaded between jobs.
- Depend heavily on a custom container environment.
- Use predictable CPU and memory for sustained processing.
That workload is starting to look less like an event handler and more like a service.
A container-based architecture may fit it more naturally:
Application Load Balancer
|
v
ECS Service
|
+-----+-----+
| |
v v
Fargate Task Fargate Task
Amazon ECS with AWS Fargate lets a team run containers without managing the underlying EC2 hosts. That gives the application a different execution model from Lambda, with more control over the container runtime and a natural home for persistent services.
The question is not whether Fargate is more advanced than Lambda. It is whether the workload behaves more naturally as a continuously running container or as discrete event-driven executions.
Signal 4: Lambda Scales Faster Than the Thing Behind It
Automatic scaling is one of Lambda's biggest advantages. It can also expose a weakness elsewhere in the architecture.
Consider:
Plain text
\+--\> Lambda \--+\+--\> Lambda \--+
API Gateway ---+--> Lambda --+----> Database
+--> Lambda --+
+--> Lambda --+
The Lambda layer can respond quickly to an increase in requests. The database, third-party API, legacy service, or connection-limited backend may not.
At that point, increasing Lambda concurrency can make the downstream problem worse. The architecture needs a capacity boundary.
For asynchronous workloads, that might mean introducing SQS:
Producer
|
v
SQS
|
| controlled consumption
v
Lambda
|
v
Database
For synchronous workloads, it might mean controlling concurrency, improving caching, changing the database access pattern, or reconsidering whether every request should reach the downstream system.
AWS documents Lambda concurrency and scaling behavior separately for a reason. A function being capable of scaling does not mean every dependency behind it should receive the same level of concurrency.
This is an important distinction when reviewing an architecture. The question should not be "Can Lambda scale to this traffic?" It should be "Can the complete request path safely handle what Lambda sends downstream?"
Signal 5: Functions Are Becoming Artificial Microservices
Lambda makes it easy to create another deployment unit. That does not mean every small piece of code deserves one.
Imagine an application starts with:
CreateOrder
The team decides to separate responsibilities:
ValidateOrder
CalculateTax
CalculateDiscount
CheckInventory
CreateOrder
WriteAuditLog
Then each function starts invoking another function, sharing data structures, depending on the same database tables, and changing whenever the same business requirement changes.
The architecture is technically distributed, but the components are not genuinely independent. Now one order request may cross six functions, six log streams, several IAM roles, multiple network boundaries, and several possible failure points before it completes.
Splitting code is useful when the pieces have meaningful boundaries. Splitting code because Lambda makes deployment units cheap can create a distributed monolith.
A better question is:
Can this function change, deploy, scale, and fail independently from the functions around it?
If the answer is consistently no, those functions may belong together. Sometimes one slightly larger Lambda is easier to understand than five tiny functions that always move as a group. In other cases, a containerized service with internal modules is a cleaner boundary. The architecture should follow ownership and behavior, not a target function count.
Signal 6: The Runtime Is Fighting the Workload
Lambda supports container image packaging, configurable memory, temporary storage, layers, and several runtime options. That covers a large range of workloads.
Still, there are applications where developers spend increasing amounts of effort making the workload fit the function environment.
Common signals include:
- Large dependencies dominate startup work.
- The application needs extensive OS-level customization.
- Large models or libraries need to remain loaded.
- Startup cost is significant compared with request processing.
- The workload depends on processes that expect to stay alive.
- CPU and memory requirements do not fit the function's natural execution pattern.
- Developers are rebuilding a traditional server inside a function.
None of these automatically disqualifies Lambda. They should trigger a fresh architecture discussion.
This is particularly common with machine learning inference, media processing, browser automation, heavy data processing, and software that was originally designed as a persistent service.
A useful rule is to stop asking:
How can we make this run in Lambda?
and ask:
If we were choosing compute from scratch for this workload, would Lambda still be our first choice?
Those are very different questions.
Signal 7: You Cannot Follow One Request Without Opening Five Log Groups
Distributed systems naturally create distributed observability. That does not mean complexity should be accepted without questioning its source.
Suppose one API request travels through:
API Gateway
|
v
Lambda A
|
v
EventBridge
|
v
Lambda B
|
v
SQS
|
v
Lambda C
|
v
Step Functions
|
v
Lambda D
This architecture might be justified. It might also be the result of several years of adding components without reviewing the complete path.
When troubleshooting requires reconstructing one user action across multiple CloudWatch log groups, traces, queues, event rules, IAM roles, and retry mechanisms, I would revisit the boundaries.
The goal is not to collapse every distributed architecture into a monolith. The goal is to make sure each boundary earns its operational cost.
This is where broader AWS development patterns become useful to understand together. Lambda, SQS, SNS, DynamoDB, infrastructure as code, and container services solve different parts of an AWS application, and knowing when to move between those patterns matters more than treating Lambda as the default destination for every piece of backend code.
Signal 8: You Are Paying for Waiting
A function can spend much of its execution doing nothing useful from a compute perspective.
For example:
Lambda starts
|
v
Call external API
|
v
Wait...
|
v
Check status
|
v
Wait...
|
v
Check again
Sometimes waiting inside a function is perfectly reasonable, especially when it is short and predictable.
It becomes questionable when the application repeatedly holds compute while waiting for something that could complete minutes, hours, or days later.
An external approval is an obvious example:
Submit Request
|
v
Wait for Human Approval
|
v
Continue Processing
That is workflow state, not useful CPU work. Step Functions, Durable Functions, queues, callbacks, and event-driven designs give you ways to separate waiting from active computation. The right choice depends on whether you want the workflow expressed through a state machine, code-first durable execution, or independent event consumers.
This is another case where "add a Lambda" may work technically while still being the wrong abstraction.
Signal 9: Your Architecture Contains Lambda Functions Named Like Connectors
Function names can reveal architecture problems.
Imagine seeing these in a repository:
sqs-to-stepfunctions
eventbridge-to-sns
s3-to-eventbridge
copy-message
forward-event
start-workflow
route-notification
Some of these functions may contain legitimate validation, enrichment, security checks, or business rules. If they do not, ask whether AWS already provides the connection.
Native service connections can reduce code and make the intent of the architecture clearer. This does not mean removing Lambda wherever possible. Custom computation belongs in Lambda.
The distinction I use is simple:
Need business logic?
|
Yes
|
v
Lambda
Need only transport/routing?
|
Maybe
|
v
Check native AWS capabilities first
Code should exist because the application needs code, not because connecting two boxes felt familiar.
Lambda vs. Fargate Is Too Small a Question
Once a workload starts pushing against Lambda, teams often jump straight to:
Should we move this to Fargate?
Sometimes yes, but there are more than two options.
A better decision tree looks something like this:
What kind of work is this?
|
+---- Short event-driven computation
| |
| v
| Lambda
|
+---- Multi-step stateful workflow
| |
| v
| Step Functions or
| Durable Functions
|
+---- Long-running/persistent container
| |
| v
| ECS + Fargate
|
+---- Message buffering
| |
| v
| SQS
|
+---- Event routing
| |
| v
| EventBridge
|
+---- Scheduled orchestration
|
v
EventBridge +
appropriate compute/workflow
The key is that some problems do not require a different compute service. They require less compute. If Lambda is being used to route an event, wait for a process, buffer traffic, store workflow state, or coordinate retries, the replacement may be a managed AWS capability rather than another runtime.
Do Not Rewrite a Healthy Lambda Architecture Just to Use Containers
Architecture discussions can swing too far in the other direction. A team sees 40 Lambda functions and assumes the application has become too complicated. Someone proposes moving everything into ECS because containers feel easier to reason about.
Function count alone does not justify that migration.
If those 40 functions are:
- Event-driven.
- Independently deployable.
- Short-lived.
- Easy to monitor.
- Cheap for the workload.
- Clearly owned.
- Loosely coupled.
- Handling bursty traffic well.
then Lambda may still be the better fit. Moving them into a continuously running container could increase operational work without solving an actual problem.
AWS's own Fargate or Lambda decision guidance makes the distinction primarily around workload characteristics. Lambda fits short-duration, event-driven tasks, while Fargate provides a container-oriented environment for persistent services and workloads needing more runtime control. The service should follow the workload, not an architectural fashion.
A Review I Would Run Before Adding the Next Function
When a pull request introduces another Lambda function, I would not ask whether the application already has too many.
I would ask:
- What event or request causes this function to run?
- What actual computation does it perform?
- Could AWS connect the source and target directly?
- Does this function own business logic or only orchestration?
- Does it need to maintain workflow state?
- How long does the work naturally take?
- What happens when it retries?
- Can it scale safely against downstream dependencies?
- Can it change independently from neighboring functions?
- Is it mostly waiting for another system?
- Would a queue solve the real problem?
- Would a workflow service make the process clearer?
- Does the workload want a persistent container instead?
- How will we trace a request through this function?
- If Lambda did not already exist in this architecture, would we choose it for this job?
That final question is the one I find most useful.
Existing architecture creates momentum. Once a team has deployment templates, logging, IAM modules, CI/CD, and monitoring built around Lambda, adding one more function becomes extremely easy. Ease of deployment should not make the architecture decision for us.
The Point Is Not to Use Less Lambda
AWS Lambda is a very good answer to a large class of problems. Event processing, API handlers, stream consumers, file processing, automation, lightweight transformations, and many other workloads fit its execution model naturally.
The goal is not to minimize the number of functions. The goal is to notice when a function has stopped being a natural unit of computation and has become a workaround for something else.
If Lambda is performing bounded event-driven work, keep it. If it is coordinating an increasingly complicated business process, consider moving the state out of the function. If it is acting only as a connector, check the native service capabilities. If it wants to stay alive indefinitely, consider a container. If it scales faster than a dependency can cope with, put a capacity boundary in front of that dependency.
And if the only reason for adding another Lambda is that the previous ten requirements also became Lambda functions, that is probably the best time to redraw the architecture before adding number eleven.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article