Introduction
Every development team has one, that critical application that runs the business, that nobody dares to touch, and that a brave team somewhere is slowly rewriting. In the meantime, the business doesn't stop. New requirements keep coming, and someone has to handle them.
The classic dilemma : do you ask the team rewriting the application to squeeze in this new feature, slowing down their already complex work ? Or do you modify the legacy codebase yourself, knowing that one wrong move could break something nobody fully understands anymore ?
There is a third option that most developers overlook : intercept the request at the load balancer level and handle it with an AWS Lambda function, without touching the legacy application at all. Not a single line of code changed. Not a single deployment risk.
In this article, you will learn how to use an Application Load Balancer (ALB) listener rule to route a specific URL path to a Lambda function, while everything else continues flowing normally to your existing EC2 application.
The Building Blocks
Before diving into the implementation, let's quickly align on the AWS building blocks involved in this solution.
Application Load Balancer (ALB)
An ALB is a managed load balancer that distributes incoming HTTP/HTTPS traffic across multiple targets. Beyond simple load balancing, it supports content-based routing, meaning it can make intelligent forwarding decisions based on the URL path, hostname, HTTP headers, or query parameters of each incoming request.
Listeners and Rules
A Listener is a process that checks for incoming connection requests on a specific protocol and port (in our case, HTTP on port 80). Each Listener has a set of Rules that define how to route traffic. Rules are evaluated in priority order, the first matching rule wins. A Default Rule handles any traffic that doesn't match any specific rule.
Target Groups
A Target Group is a logical grouping of destinations that the ALB forwards traffic to. AWS supports two target types relevant to our use case : Instance (your EC2 application) and Lambda (your function). Each Target Group handles health checking independently; note that Lambda Target Groups do not require health checks, since Lambda manages its own availability.
AWS Lambda
Lambda is a serverless compute service that runs your code in response to events, including HTTP requests forwarded by an ALB. You only pay for the actual execution time, making it a cost-effective choice for handling specific URL paths that don't receive constant traffic.
The Architecture
Now that we have a clear picture of each component, let's see how they work together.
The solution relies on a single Application Load Balancer serving as the single entry point for all incoming traffic. Behind it, two distinct Target Groups handle requests based on the URL path :
- The Instance Target Group receives all general traffic and forwards it to the legacy EC2 application, business as usual.
- The Lambda Target Group receives only requests matching a specific URL path (in our case
/lambda) and forwards them to the new Lambda function.
Here is the complete request flow :
Client Request
│
▼
Application Load Balancer (port 80)
│
▼
Listener — evaluates rules in priority order
│
├── Rule: path is /lambda ──────────────► Lambda Target Group ──► Lambda Function
│ │
└── Default Rule ──────────────────────► Instance Target Group ──► EC2 Application
The key insight here is that the ALB makes the routing decision before the request ever reaches any backend. Neither the Lambda function nor the EC2 application knows about each other, they simply receive requests and return responses. The ALB handles all the traffic orchestration.
Why This Matters
This architecture gives you surgical precision : you can intercept exactly one URL path and handle it differently, while leaving 100% of the existing application behavior untouched. No redeployment of the legacy app. No coordination with the team rewriting it. No regression risk.
Step-by-Step Implementation
Let's walk through the exact steps to implement this architecture. We assume the following resources already exist in your AWS account : an Application Load Balancer, an EC2 instance running your legacy application, and a Lambda function containing your new feature's logic.
Step 1: Create the Lambda Target Group
In the EC2 console, navigate to Target Groups and create a new Target Group with the following settings :
- Target type : Lambda function
- Target Group name : something containing "Lambda" for clarity (e.g.
tg-lambda-new-feature) - Health checks : disabled, Lambda manages its own availability, no health check endpoint is needed
Once created, register your Lambda function as the target.
Step 2: Create the Instance Target Group
Create a second Target Group for your EC2 application :
- Target type : Instances
- Target Group name : something containing "Instance" (e.g.
tg-instance-legacy-app) - Protocol : HTTP, Port : 80
- Health check path : /health, make sure your legacy application exposes this endpoint and returns a 200 response
Register your EC2 instance as the target and wait for its status to become Healthy before proceeding.
Step 3: Add a Listener to the ALB
In the EC2 console, navigate to Load Balancers, select your ALB, and add a Listener :
- Protocol : HTTP, Port : 80
- Default action : Forward to the Instance Target Group
This ensures that all traffic goes to your legacy EC2 application by default, exactly as before.
Step 4: Add a Routing Rule for Lambda
In your Listener, add a new Rule with the following configuration :
- Condition : Path is
/lambda - Action : Forward to the Lambda Target Group
- Priority : set it higher than the Default Rule (a lower number means higher priority)
From this point on, any request to /lambda is intercepted by the ALB and forwarded to your Lambda function. Everything else continues flowing to EC2.
Step 5: Ensure your Lambda Returns the Correct Response Format
This is a critical detail that catches many developers off guard. When invoked by an ALB, Lambda must return a response in a specific format, different from the API Gateway format :
{
"statusCode": 200,
"statusDescription": "200 OK",
"isBase64Encoded": false,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"message\": \"Hello from Lambda\"}"
}
If your Lambda returns a response in the API Gateway format (without statusDescription), the ALB will return a 502 Bad Gateway error to the client. Always validate your response format when switching between ALB and API Gateway integrations.
Common Pitfalls to Avoid
Even with a solid understanding of the architecture, a few subtle mistakes can cost you hours of debugging. Here are the most common ones.
Pitfall 1: Forgetting to Disable Health Checks on the Lambda Target Group
Unlike EC2 instances, Lambda functions do not listen on a port and cannot respond to traditional HTTP health check probes. If you enable health checks on a Lambda Target Group, the ALB will continuously mark your Lambda as unhealthy and stop routing traffic to it. Always disable health checks when using Lambda as a target.
Pitfall 2: Wrong Lambda Response Format
As mentioned in the implementation steps, the ALB expects a very specific response structure from Lambda, including the statusDescription field. A response that works perfectly with API Gateway will silently break when used behind an ALB, returning a 502 to the client with no obvious error message in the Lambda logs. Always test your Lambda response format specifically in the ALB context.
Pitfall 3: Rule Priority Order
ALB Listener Rules are evaluated in priority order, lowest number wins. If your Lambda rule has a lower priority than another rule that matches the same path, your Lambda will never receive traffic. Always double-check the priority of your rules, especially when adding new ones to an existing Listener that already has multiple rules.
Pitfall 4: Missing Lambda Permissions
The ALB needs explicit permission to invoke your Lambda function. When you register a Lambda function via the AWS console, this permission is added automatically as a resource-based policy on the Lambda. However, if you use the AWS CLI or infrastructure-as-code tools like Terraform or CloudFormation, you must add this permission manually using lambda:AddPermission with the ALB as the principal. Without it, the ALB will receive a permission denied error and return a 502 to the client.
Pitfall 5: One Lambda per Target Group
Unlike Instance Target Groups, a Lambda Target Group can only contain a single Lambda function. If you need to handle multiple distinct URL paths with different Lambda functions, you must create a separate Target Group for each one and add a corresponding Listener Rule for each path.
Infrastructure as Code: Terraform Implementation
If you prefer infrastructure-as-code over manual console steps, and you should, here is how to implement the key pieces of this architecture using Terraform. The code follows a modular structure for reusability and clarity.
The full Terraform code, including module structure, variables, and README, is available on GitHub : alb-lambda-legacy-pattern
Here are the two most critical resources to understand.
The Listener Rule, routing /lambda to the Lambda Target Group
resource "aws_lb_listener_rule" "lambda_rule" {
listener_arn = aws_lb_listener.http.arn
priority = 10 # Lower number = higher priority
condition {
path_pattern {
values = ["/lambda"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.lambda_tg.arn
}
}
The Lambda Permission, the step the console handles automatically but Terraform requires explicitly
resource "aws_lambda_permission" "alb_invoke" {
statement_id = "AllowALBInvoke"
action = "lambda:InvokeFunction"
function_name = var.lambda_function_arn
principal = "elasticloadbalancing.amazonaws.com"
source_arn = var.alb_arn
}
This second block is the most commonly forgotten piece when implementing this pattern with Terraform or the CLI. The console adds it silently, Terraform will fail at the Target Group attachment step without it.
The full repository includes the complete module structure (target_groups, listener, lambda_permission), all variable definitions, and a README.md explaining prerequisites and how to apply the code against existing infrastructure.
Conclusion
ALB vs API Gateway : When to Use Which ?
A natural question arises after reading this article : if I can route traffic to Lambda via an ALB, why would I ever use API Gateway ?
The answer depends on your use case :
Use ALB + Lambda when you already have an ALB in front of an EC2 or container-based application and you want to handle specific paths with Lambda without introducing a new service into your architecture. It is simpler, cheaper at high traffic volumes, and integrates naturally into an existing infrastructure.
Use API Gateway + Lambda when you are building a dedicated REST API from scratch, need advanced features like request/response transformation, authentication with custom authorizers, usage plans, throttling per consumer, or WebSocket support. API Gateway is purpose-built for API management and offers a richer feature set.
In short : ALB is the right choice when Lambda is one piece of a larger existing architecture. API Gateway is the right choice when Lambda is the architecture.
The pattern you learned in this article is not just a clever trick, it is a pragmatic engineering decision that lets your team move fast without breaking what already works. And in software engineering, that is often the most valuable skill of all.
Top comments (0)