API Gateway or ALB in Front of Lambda

November 02, 2026 · 17 min read

Solutions Architect · SAA-C03 · part of The Exam Room

The situation

The team runs an internal webhook receiver: third-party payment providers POST signed payloads to https://webhooks.internal.example.com/provider/*, our Lambda validates the signature, persists the event to EventBridge, and returns 200. Current numbers:

  • ~200 requests per second average, peaking at 1200 during provider retries after their own outages.
  • Payload sizes between 2 KB and 80 KB. No streaming; every webhook is a bounded POST.
  • Five providers, each with their own signing scheme. Five Lambda functions, one per provider, routed by URL path.
  • Authentication is HMAC over the body, handled inside the Lambda, not at the front door.
  • Latency target: p95 under 400ms end to end, of which the front door should contribute under 20ms.
  • The service is VPC-adjacent: it writes to EventBridge (no VPC needed) but also talks to an ElastiCache cluster inside the VPC for idempotency tracking.

Today the team uses API Gateway REST API. The monthly bill has API Gateway request charges as the biggest line. Someone spotted the ALB+Lambda pattern in a re:Invent talk and asked “could we do that instead and save money?” Finance cares about the bill; engineering cares about whether a cheaper front door loses something the current one does.

What actually matters

Before benchmarking prices, it’s worth naming what a front door actually does for a Lambda.

The first thing a front door does is terminate HTTPS and shape the request into a Lambda event. Both API Gateway and ALB do this. The event shapes differ: API Gateway (v1 REST) sends a rich event with resource, path, pathParameters, queryStringParameters, headers, multiValueHeaders, requestContext.identity, body, and more. ALB sends a simpler event with httpMethod, path, queryStringParameters, headers, body, and isBase64Encoded. The handler code has to speak whichever shape the front door speaks. Migrating between them is not zero work.

The second thing a front door provides is request-level features: authentication, authorisation, throttling, usage plans, API keys, request validation, caching, transformations, CORS handling, WAF integration. API Gateway has all of them as first-class features; ALB has fewer (CORS at the listener, WAF on the ALB, OIDC authentication at the listener for some use cases). Whether that matters depends on whether the Lambda wants them. In our case the Lambda handles auth itself. HMAC over the body, which neither front door can do, so a lot of API Gateway’s auth machinery would sit unused.

The third thing is pricing shape. The two options charge in fundamentally different shapes: one is per-request, where every webhook adds a cent fraction to the bill; the other is per-hour plus a capacity-unit metric (new connections, active connections, bytes, rule evaluations), where the bill is roughly flat below a high traffic floor. At the kind of traffic this service runs the gap between the two shapes is significant; below a few hundred thousand requests a month the hourly model is cheaper, above it the gap widens.

The fourth thing is operational model. API Gateway stages, deployments, resource policies, stage variables, throttling settings, Lambda authorizers. An ALB has listeners, listener rules, target groups, target attributes. Two different mental models, two different IaC surfaces, two different ways of saying “route POSTs on /provider/stripe to Lambda function X.” Neither is wrong; switching costs familiarity.

The fifth thing is scale behaviour. Both options scale well past this workload’s 1200 rps burst, with different ceilings and different knobs. The managed-API option has account-level rps caps that are soft and raisable, with per-client throttling as a first-class feature; the load-balancer option scales as fast as the function’s concurrency allows, with its own capacity-unit ceiling at a much higher traffic level than this service will see.

The sixth thing is integration with VPC and private services. API Gateway REST has private APIs via VPC endpoints; API Gateway HTTP can be put behind a private ALB. ALB itself lives in subnets (public or private) and is a VPC-native thing. If the front door needs to be internal-only and reachable via private DNS from on-prem over a Direct Connect, an internal ALB is the more straightforward path, which matches “webhooks.internal.example.com” suggesting providers reach us via some private connectivity.

What we’ll filter on

Filters for the two options:

  1. Request-level features (auth, throttling, validation, API keys), how much does the front door do vs the function?
  2. Cost at this traffic volume, monthly bill at 200 rps average, ~500M requests/month.
  3. Lambda event shape fit, does the current handler code work as-is?
  4. VPC and private connectivity, can this be an internal-only endpoint with private DNS?
  5. Operational familiarity and observability, stages/deployments vs listeners/target groups; metrics and logs.
  6. Headroom for growth, what happens when the service grows or changes shape (WebSocket, streaming, larger payloads, per-client throttling)?

The front-door landscape

  1. API Gateway REST API. The feature-rich option. Stages, deployments, API keys, usage plans, request validation, Lambda authorizers, mapping templates, VTL transformations, response caching, full WAF integration, private APIs via interface VPC endpoints. Charges per request. Event shape is the rich v1 event. Default throttling at 10,000 rps burst, 5,000 rps steady per account per Region. Best fit when the front door is genuinely doing API-management work, not just proxying to Lambda.

  2. API Gateway HTTP API. The stripped-down sibling. Lower per-request cost (~$1.00/million vs $3.50/million for REST), simpler event shape (v2, closer to ALB’s), JWT authorizers instead of Lambda authorizers, fewer transformation features, no private API (but can be put behind a private ALB or use PrivateLink). Fit when the team wants API Gateway’s scaling and auth story but doesn’t need REST API’s full feature matrix.

  3. ALB with Lambda target. The infrastructure-native option. An ALB with a target group whose target type is lambda. Listener rules route /provider/stripe to one function, /provider/adyen to another, etc. ALB terminates TLS, runs WAF, can do OIDC authentication at the listener, streams access logs to S3. No API keys, no usage plans, no request validation. LCU-based pricing. Event shape is ALB’s simpler event.

  4. Function URL. Each Lambda function has an optional built-in HTTPS endpoint (https://<url-id>.lambda-url.<region>.on.aws). No front door at all. Lambda handles TLS and request routing directly. Auth is either IAM (AWS_IAM auth type) or none. Custom domains require CloudFront in front. Free; you pay only for Lambda invocations. Fit when the service is one Lambda with one path and the auth model is IAM or self-managed inside the handler.

Side by side

Option Request features Cost at 500M/mo Event shape fit Private VPC endpoint Ops familiarity Growth headroom
API Gateway REST ✓✓ $1,750 requests + DT Current code ✓ (private API) Stages/deployments ✓✓ (WS, caching, keys)
API Gateway HTTP $500 requests + DT Code change ✗ direct (proxy needed) Similar to REST ✓ (JWT, simpler)
ALB + Lambda — (WAF, OIDC) ~$26 ALB + LCU (~$60) Code change ✓ (internal ALB) Listeners/target groups ✓ (other targets alongside)
Function URL $0 + invocations Code change ✗ (needs CloudFront) Minimal ✗ (single function)

Reading the table at 500M requests/month:

  • API Gateway REST: ~$1,750/month in request charges alone. Expensive, but we get every feature.
  • API Gateway HTTP: ~$500/month. Cheaper, similar model, loses REST-specific features.
  • ALB: ~$86/month, order-of-magnitude cheaper than either. Loses API-management features entirely.
  • Function URL: ~$0 extra. Loses front-door features entirely, plus you can’t route by path across multiple functions.

For this webhook receiver, five Lambdas routed by path, auth done inside the function, no API keys, no throttling requirements, internal-only endpoint, the ALB answer is sound. For a public API with third-party consumers who need rate-limiting, API keys, or usage-based billing, the API Gateway answer is sound. The correct choice is not about which is cheaper in the abstract; it’s which is cheaper given the features you actually use.

Routing a webhook to the correct Lambda

API Gateway REST Client POST /provider/stripe HMAC in body, signature in X-Signature header API Gateway REST API (prod stage) Resources: /provider/{name} Method: POST · Integration: AWS_PROXY Stage variables: alias=live Event shape: v1 (rich) resource, pathParameters, requestContext, ... stripe-fn :live adyen-fn :live wise-fn :live Pricing at 500M requests/month ~$1,750 /mo $3.50 per million requests + DT out (small for webhook 200 responses) + per-client throttling, API keys, usage plans (unused here) ALB + Lambda target Client POST /provider/stripe HMAC in body, signature in X-Signature header Internal ALB (private subnets) Listener :443 · TLS cert from ACM WAF Regional attached Listener rules: path-pattern → target group Event shape: ALB event httpMethod, path, headers, body, isBase64Encoded stripe-fn alias live adyen-fn alias live wise-fn alias live Pricing at 500M requests/month ~$86 /mo $0.0225/hr × 730 hr = ~$16 LCUs (new conns, bytes, rules) ≈ $70 no API keys, no usage plans, no request validation
Same five Lambdas, two front doors. API Gateway brings features this webhook receiver doesn't use at a price that scales with every request. ALB brings a VPC-native endpoint with per-LCU pricing at a fraction of the cost, losing features the service never consumed.

The pick in depth

For this webhook receiver, ALB with Lambda targets is the correct answer. The service doesn’t use API Gateway’s feature surface: no API keys, no usage plans, no per-client throttling, no request validation, no mapping templates. Auth happens inside the Lambda because HMAC-over-body is something neither front door can do. The endpoint needs to be internal-only and reachable via private DNS from connected VPCs and on-prem, an internal ALB in private subnets is the most direct path.

The listener rules. One listener on port 443 with a rules chain:

priority 10: host-header=webhooks.internal.example.com AND path-pattern=/provider/stripe → tg-stripe
priority 20: host-header=webhooks.internal.example.com AND path-pattern=/provider/adyen  → tg-adyen
priority 30: host-header=webhooks.internal.example.com AND path-pattern=/provider/wise   → tg-wise
...
priority 999: default → fixed-response 404

Path patterns support a handful of wildcards (* and ?) and the rules engine evaluates in priority order. Each target group has one Lambda target; target type lambda and the target is a Lambda alias ARN (arn:aws:lambda:eu-west-1:111122223333:function:stripe-fn:live).

Permission to invoke. The ALB invokes the Lambda using a resource-based policy on the function:

aws lambda add-permission \
    --function-name stripe-fn \
    --statement-id alb-invoke \
    --action lambda:InvokeFunction \
    --principal elasticloadbalancing.amazonaws.com \
    --source-arn arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/tg-stripe/a1b2c3d4

Creating the target group via the EC2/ELBv2 console does this for you; IaC setups need to remember it. Missing this permission is the classic first-time error and surfaces as “Target.FailedHealthChecks” with no obvious reason.

Event shape translation. The Lambda code currently reads event.body as a string (both shapes have this) and parses event.headers['x-signature'] (both shapes have it, but ALB lowercases headers while API Gateway REST v1 does not). The handler needs a small shim that normalises headers to lowercase and checks event.requestContext?.elb to detect the ALB shape versus the API Gateway shape. Not a rewrite; a ~20-line adaptation per function.

VPC, DNS, and certificate. The internal ALB sits in two private subnets. Route 53 private hosted zone internal.example.com has an alias record webhooks.internal.example.com pointing to the ALB’s DNS name. The certificate on the listener is an ACM-issued cert for webhooks.internal.example.com, validated via DNS. Providers reach the ALB via our connectivity story (Direct Connect or a partner VPN), resolving the name through a Route 53 Resolver inbound endpoint.

WAF. WAF Regional attached to the ALB with a small rule set: AWS managed rules for common attacks, an IP set allow-list for the providers’ publish IP ranges, rate-based rules at 2000 requests per 5 minutes per IP. Blocks at the WAF layer cost no Lambda invocation.

Observability. ALB access logs to S3 (one row per request: client IP, path, status, Lambda duration, bytes). CloudWatch metrics on the target group (RequestCount, TargetResponseTime, HTTPCode_Target_5XX_Count, TargetConnectionErrorCount). Lambda has its own logs in CloudWatch. X-Ray captures the ALB → Lambda → EventBridge trace if tracing is enabled on the function (ALB doesn’t currently add its own segment, but the Lambda segment starts from the ALB’s request ID).

A worked migration

The Stripe webhook moves first as a canary.

# 1. Create target group
$ aws elbv2 create-target-group \
    --name tg-stripe \
    --target-type lambda

# 2. Register Lambda alias as target
$ aws elbv2 register-targets \
    --target-group-arn arn:aws:elasticloadbalancing:eu-west-1:...:targetgroup/tg-stripe/... \
    --targets Id=arn:aws:lambda:eu-west-1:111122223333:function:stripe-fn:live

# 3. Grant invoke permission
$ aws lambda add-permission --function-name stripe-fn --qualifier live \
    --statement-id alb-invoke --action lambda:InvokeFunction \
    --principal elasticloadbalancing.amazonaws.com \
    --source-arn arn:aws:elasticloadbalancing:eu-west-1:...:targetgroup/tg-stripe/...

# 4. Create listener rule on existing internal ALB
$ aws elbv2 create-rule \
    --listener-arn arn:aws:elasticloadbalancing:eu-west-1:...:listener/... \
    --priority 10 \
    --conditions Field=host-header,Values=webhooks.internal.example.com \
                 Field=path-pattern,Values=/provider/stripe \
    --actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:eu-west-1:...:targetgroup/tg-stripe/...

The platform keeps the API Gateway live and ships a second provider-side config that sends traffic to https://webhooks.internal.example.com/provider/stripe (the ALB) for 10% of webhooks. CloudWatch Logs Insights on the stripe-fn log group shows both paths working; she compares error rates and response times for a day. Both at roughly 140ms p95, both at zero 5xx. She flips the remaining 90% over, decommissions the API Gateway resource for Stripe, and repeats for the other four providers over the next two weeks.

The bill for the webhook receiver drops from $1,750 to $86 per month. The Lambda invocation charges, the real cost of the service, are unchanged.

When API Gateway is still the correct answer

The migration made sense for this service. Three scenarios where the reverse answer is correct:

  1. Public API with third-party developers. API keys, usage plans, documentation (via the Developer Portal), and per-client throttling are what API Gateway is for. An ALB has none of these.
  2. Request validation at the edge. If every request must match a JSON schema and invalid ones should never reach Lambda, API Gateway’s request validation saves Lambda invocations. ALB passes everything through.
  3. Response caching. API Gateway has a managed response cache keyed on query string or headers. ALB doesn’t. For a read-heavy API where cache hit rates are high, API Gateway cache savings can dwarf the per-request premium.

Even for an internal service, if the team wants per-consumer quotas or detailed usage metering, API Gateway has the machinery and ALB doesn’t.

What’s worth remembering

  1. Both front doors terminate HTTPS and invoke Lambda, and that’s about where the overlap ends. API Gateway is an API-management product; ALB is a load balancer. The features they each bring are a good guide to the situations they’re suited to.
  2. Pricing shapes diverge at scale. API Gateway charges per request ($3.50/M REST, $1.00/M HTTP). ALB charges per hour plus LCUs. At a few hundred million requests/month ALB is an order of magnitude cheaper, but you lose API-management features.
  3. The event shape matters. v1 (REST) is rich; v2 (HTTP) and ALB’s event are simpler. Handlers often need a shim when moving between them, cheap but not free.
  4. Request-level auth is a feature to look at twice. If the Lambda does its own auth (HMAC, custom tokens), neither front door’s auth features add value. If the front door can do the auth, doing it at the door saves Lambda invocations.
  5. Internal endpoints in a VPC favour ALB. ALB is VPC-native; API Gateway REST has private APIs via VPC endpoints but the ergonomics are more moving parts. Internal-only webhook receivers land more simply on ALB.
  6. Function URL is a third option for single-function, single-path services. Built into Lambda, no charge for the front door itself, only IAM or “none” for auth, no path routing across multiple functions.
  7. Pair an ALB front door with Lambda aliases for safe deploys. Register the alias as the target; alias routing shifts traffic at the Lambda side without touching the target group.
  8. Pick based on the features you actually use. API Gateway’s feature list is its value proposition. If you use most of it, the cost is earned. If you use none of it, an ALB is a lot of money saved for features you weren’t consuming.

A front door for a function is a small decision that compounds across millions of requests. The correct one for a webhook receiver with in-handler auth and internal-only reach is the cheaper, less-feature-rich one; the correct one for a rate-limited public API with API keys is the feature-rich one that charges for every request. Match the door to what the service actually does at its door.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.