They're three levels of abstraction. The higher you go, the less server you manage — but the less you control, and the cost model changes entirely.
I ask myself three questions, in this order.
Event-driven, rare spikes, or near-zero most of the time → Lambda. You pay nothing while it sleeps.
An API running 24/7, already in Docker, several services → ECS/Fargate. The sweet spot.
Legacy PHP, GPU, precise system config, or you want the lowest cost at stable load → EC2.
The smaller you are, the higher you go: let AWS manage the machines for you.
For EC2, deployment is the one from my Langfuse and e-commerce guides: machine, reverse proxy, HTTPS, pm2 or Docker.
There's no absolute winner: the right choice depends on your traffic curve. In doubt on a stable load, ECS/Fargate is rarely a bad bet.
The tree above says “intermittent traffic → Lambda”, but before putting everything there you need to know three walls I hit regularly in production. They’re what decide whether Lambda holds up or not.
A single invocation can’t exceed 900 seconds — a hard, non-negotiable ceiling, with at most 10,240 MB of RAM ([Lambda quotas](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html)). A batch that runs over → you split it, move to Step Functions, or push the task onto Fargate.
By default, 1,000 concurrent executions for your whole account in a region, shared across all your functions ([function scaling](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html)). Past that, throttling: requests get dropped. It’s raisable, but you have to ask — and remember reserved concurrency to protect your critical functions.
When no warm environment is available, Lambda spins up a fresh one: that’s the cold start, from a few hundred ms to several seconds depending on runtime and your dependencies. Painful on a user-facing API, invisible on batch.
Java, Python 3.12+, .NET 8+
all runtimes
My rule: SnapStart first when the runtime supports it (it’s free), provisioned concurrency only if I have a strict latency SLA that SnapStart can’t meet. And if functions run long or continuously, I re-read the tree — often that’s the sign it’s time to move to ECS.
Once you’ve chosen containers, the sub-choice remains: Fargate (AWS manages the machines) or the EC2 launch type (you manage your fleet under ECS). It’s not religion, it’s a matter of utilization rate.
The AWS Fargate vs EC2 blog puts numbers on the crossover: at low reservation rates, Fargate can be up to ~87% cheaper than an under-used instance; at full load, the EC2 launch type pulls ahead with over 20% savings. The tipping point is your ability to keep instances filled.
Many teams end up mixed: Fargate for spiky or low-use services, EC2 for the stable core that justifies a Savings Plan. You’re never forced to pick one launch type for the whole cluster.