System design · AWS

Lambda vs Fargate vs ECS: a decision framework, not a religion

Compute choice is a system-design decision about traffic shape, failure isolation, and who wakes up at night. Here is the framework I use in design reviews instead of “we are a serverless shop.”

LambdaECS FargateECS on EC2API GatewayALBCloudWatch

I have shipped production APIs on all three. The worst designs were the ones that started from a platform preference. The useful question is: what does a single unit of work look like, how spiky is it, and what happens when that unit hangs?

Start from the traffic shape

  • Spiky, idle most of the day, execution under 15 seconds, no sticky sockets: Lambda. Payment webhooks, image variants, IoT alarms, cron-like glue.
  • Steady RPS, need long-lived connections (WebSocket, gRPC, SSE), or a language runtime that hates cold starts: Fargate behind an ALB. Nest.js BFFs and Angular Universal belong here more often than people admit.
  • High, predictable baseline and a team that already operates AMIs: ECS on EC2. You are buying reserved-instance economics and a daemon story, not novelty.

The constraints that actually bite

Lambda’s 15-minute ceiling is not the usual problem. Payload size, ephemeral disk, and the fact that a burst of 3,000 concurrent executions can surprise a downstream RDS are. I put a SQS buffer in front of any Lambda that talks to a database that cannot scale with the function. Fargate’s problem is the opposite: it will sit there costing money at 3 a.m. with one task, and rolling deploys are your responsibility. ECS on EC2 adds capacity-provider math. If nobody on the team can explain a drain of a spot instance, do not choose it for a customer-facing API.

A mixed system I will actually sign off on
CloudFront
  → S3 / SSR (Angular Universal on Fargate)
  → ALB → BFF (Nest.js, Fargate, min 2 tasks / AZ)

API Gateway HTTP API
  → Lambda: webhooks, async commands
  → SQS → Lambda workers (isolated from RDS via pool proxy / RDS Proxy)

ECS scheduled tasks: Glue-adjacent jobs that are too long for Lambda
                           and too rare to keep a service warm

Cost is a function of idle, not of logo

Lambda looks cheap until the account is dominated by provisioned concurrency you added to hide cold starts. Fargate looks expensive until you count the engineer-weeks of a custom scale-to-zero. I model three numbers in a review: p99 latency budget, monthly request volume, and idle hours. If idle hours dominate, Lambda or scale-to-zero Fargate. If p99 is tight and the runtime is Node with a fat framework, pay for warm tasks and stop pretending cold starts are a config flag.

Pick the boring default for the shape you have, then mix. Homogeneous compute is a team-scaling tactic, not an architecture.