Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 019 / 100

ALB listener rules: what every AWS developer should know

One load balancer, many services.

Expona Admin8 min read

One load balancer can serve multiple applications, microservices, and environments—but only if you route traffic intelligently. Today we're covering ALB listener rules, the mechanism that lets a single Application Load Balancer handle dozens of backends without collision. I've used this pattern across every platform I've built, from splitting admin dashboards away from customer APIs to redirecting legacy traffic seamlessly.

Why ALB listener rules matter

When you're running multiple services on AWS—perhaps a REST API, a real-time WebSocket server, an admin panel, and a static website—you face a choice: run separate load balancers (expensive, complex) or use one ALB with listener rules (scalable, cost-effective). Listener rules let you define conditions and actions that decide which backend receives each request before it ever reaches your instances.

Think of it like a postal sorting facility. Instead of hiring separate postmen for each address, you hire one postman with a rulebook: "If the address says 123 Main St, go to the residential cluster. If it says 456 Commerce Ave, go to the business cluster." ALB listener rules are that rulebook.

Route by host: api.example.com

The most common pattern I've implemented is host-based routing. You might have:

  • api.example.com → backend API service (port 8000)
  • admin.example.com → admin dashboard (port 3000)
  • example.com → marketing site (port 80)
  • cdn.example.com → media server (port 9000)

All these subdomains point to the same ALB's DNS name via CNAME records. The ALB examines the Host header in the HTTP request and routes accordingly. This is beautifully clean because each service team can own their own target group, health checks, and scaling policies independently.

Here's how to set it up in Terraform (simplified):

bash
resource "aws_lb_listener_rule" "api_host_route" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 1

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api_backend.arn
  }

  condition {
    host_header {
      values = ["api.example.com"]
    }
  }
}

resource "aws_lb_listener_rule" "admin_host_route" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 2

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.admin_dashboard.arn
  }

  condition {
    host_header {
      values = ["admin.example.com"]
    }
  }
}

Notice the priority field. Rules are evaluated top-to-bottom by priority number (lower = higher priority). If two rules match, the one with lower priority wins. I always leave gaps (1, 2, 10, 20) so I can insert rules later without renumbering everything.

In production, I've used host-based routing to isolate a heavy analytics API from the main customer-facing API. Both live in the same ALB, but I could scale, patch, or troubleshoot them independently. It also lets you use separate SSL certificates per subdomain if needed (though AWS lets you bundle them).

Route by path: /admin/*

Path-based routing is your second tool. Same load balancer, same host, different paths go to different targets:

  • /api/v1/* → API backend
  • /admin/* → Admin panel
  • /webhooks/stripe/* → Webhook processor
  • / (default) → Frontend SPA

This is valuable when you're consolidating multiple services behind a single domain. I've used it in ride-hailing platforms where /driver/* routes to the driver backend, /passenger/* routes to a different backend optimized for different queries, and /admin/* goes to an internal tool.

bash
resource "aws_lb_listener_rule" "admin_path_route" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 10

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.admin_panel.arn
  }

  condition {
    path_pattern {
      values = ["/admin/*"]
    }
  }
}

resource "aws_lb_listener_rule" "api_path_route" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 11

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api.arn
  }

  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }
}

Path patterns support wildcards. /admin/* matches /admin/users, /admin/settings/billing, and deeper paths. If you need exact matching, omit the wildcard: /health matches only /health, not /health/check.

One gotcha: path-based routing works for stateless services, but if your admin panel relies on session cookies or sticky sessions, you'll want to pair it with stickiness rules on that target group. I've seen developers assume ALB would magically keep sessions sticky just because they routed by path—it won't.

Redirect HTTP to HTTPS

Security-first architecture means every request should be encrypted. Rather than handling redirects in application code (which adds latency and complexity), ALB can do it for you with a redirect action.

bash
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate.main.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.api.arn
  }
}

This is a listener-level action (not a listener rule). Every HTTP request gets a 301 permanent redirect to HTTPS, preserving the path and query string automatically. I use HTTP_301 for permanent redirects (good for SEO and caching) unless the redirect needs to change, then HTTP_302 for temporary ones.

One detail: the redirect happens at ALB, so your backend never processes the HTTP request. This saves compute and is blazingly fast. However, if you're debugging and need to see what the original request looked like, you'll need to check ALB access logs, not application logs.

Fixed responses for maintenance

Deploying a service and need to take it down momentarily? Instead of failing all requests, return a proper maintenance response directly from the ALB.

bash
resource "aws_lb_listener_rule" "maintenance_response" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 5

  action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/html"
      message_body = "<h1>Maintenance in progress</h1><p>We'll be back online at 2 AM UTC.</p>"
      status_code  = "503"
    }
  }

  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }
}

resource "aws_lb_listener_rule" "health_check_ok" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 3

  action {
    type = "fixed-response"

    fixed_response {
      content_type = "application/json"
      message_body = "{\"status\":\"ok\"}"
      status_code  = "200"
    }
  }

  condition {
    path_pattern {
      values = ["/health"]
    }
  }
}

Fixed responses are also perfect for health check endpoints that don't need to hit your backend. Return a 200 immediately from ALB instead of forwarding to an instance. This saves backend load and improves response time.

I use status code 503 (Service Unavailable) for maintenance so upstream systems (CloudFront, clients, monitoring tools) know it's temporary, not a permanent redirect. If you return 404, clients might cache that and stop trying. With 503, they know to retry.

Combining rules: real-world example

Let me show you how these combine in a SaaS I built with separate billing, API, and admin services:

Priority Condition Action
1 Host = health.internal.example.com Fixed response 200
2 Path = /health Fixed response 200
10 Host = api.example.com Forward to API target group
11 Host = admin.example.com Forward to Admin target group
12 Path = /billing/* + Host = example.com Forward to Billing target group
20 Path = /api/* + Host = example.com Forward to API target group
100 Host = example.com (default) Forward to Frontend target group

Rules are checked top-to-bottom. The first matching rule wins. So a request to api.example.com/health matches priority 10 (host rule) before it could match priority 2 (path rule), and goes to the API service.

Common mistakes

  • Forgetting that lower priority numbers win: I've seen developers set priority 100 for what they thought was their most important rule. It will never match if there's a rule with priority 1. Use 1, 10, 20, etc. and think of it as a top-to-bottom checklist.
  • Not accounting for default actions: Every listener must have a default action. If no rule matches, the default action applies. If you don't set it explicitly, you'll get an error. Make your default action something sensible, like forwarding to a catch-all target group or returning a 404.
  • Assuming ALB rewrites paths: If you route /admin/* to a backend expecting /*, the backend receives /admin/something, not /something. ALB doesn't strip the matched path by default. You'll need application-level rewriting or a proxy like nginx in front of your backend.
  • Multiple conditions with OR instead of AND: When you add multiple conditions to one rule, they're AND-ed together. If you want "Path = /admin OR Path = /management," you need two separate rules.
  • Not testing with curl: Always verify rules with curl before pushing to production: curl -H "Host: api.example.com" http://your-alb-dns/test. It catches routing mistakes that you won't see in your browser (which might cache or pre-populate the Host header).

Key takeaways

  • ALB listener rules let you route traffic based on host header, path, HTTP method, source IP, and other conditions without running multiple load balancers.
  • Priority determines evaluation order; lower numbers are checked first. Always leave gaps so you can insert rules later.
  • Host-based routing is ideal when services own their own subdomains; path-based routing works when services share a domain.
  • Use redirect actions for HTTP to HTTPS conversion at the load balancer, not in your application code.
  • Fixed responses are perfect for health checks and maintenance windows, saving backend load and providing clean HTTP status codes.
  • Combine multiple conditions with AND logic; use separate rules if you need OR.
  • Test routing rules with curl before deploying; browser caching can hide routing issues.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles