Lambda vs Fargate vs EC2: When Each Wins
7 min readBytePatterns
Lambda vs Fargate vs EC2: what you deploy and manage, how each one scales and bills, the 15-minute Lambda limit, and why utilisation decides which is cheapest.
Lambda vs Fargate vs EC2 in one line: Lambda runs a function per request and you manage nothing below the code, Fargate runs a container task and you manage the container, and EC2 runs an instance and you manage the operating system. Lambda wins on spiky, event-driven work, Fargate on long-running services you do not want to patch hosts for, and EC2 on special hardware, licensing or full control. What decides cost in between is utilisation, not the price list. Every limit below comes from the AWS documentation pages listed at the end, as of October 2026, with no prices.
Side by side:
| Option | You deploy and manage | Scales by |
|---|---|---|
| Lambda | a function; no servers, no containers | request, with a cold start per new environment |
| Fargate | a container task with its CPU and memory; no instances | task, through service auto scaling |
| EC2 | an instance, its OS and everything on it | instance, through an Auto Scaling group |
The problem it solves
"Where should this run?" trades control for operations work, and three questions decide it:
- How much do you want to manage? Patching, capacity, scaling policies.
- How does traffic arrive? In bursts of events, or as a steady stream of requests.
- What does the work need? A GPU, a long-running process, a licence tied to a host.
The container orchestrator is a separate question, covered in ECS vs EKS vs Fargate. This article is about the compute underneath and when each wins.
The intuition
Lambda: the unit is one invocation. Lambda creates execution environments on demand; a new one pays an init phase, the cold start, and a warm one is reused. Code can run for up to 15 minutes in a single invocation (900 seconds), with up to 10,240 MB of memory, and CPU allocated in proportion to memory. Concurrency is shared across the Region, 1,000 by default. You are billed per request and for the time your code runs at its configured memory, so an idle function costs nothing.
Fargate: the unit is a task. You package the application in containers, specify CPU and memory, and launch; there are no servers or EC2 clusters to provision, configure or scale. Each task has its own isolation boundary and does not share its kernel, CPU, memory or network interface with another task. You pay for the CPU and memory a task is sized for, for as long as it runs, so a half-busy task is half wasted. Some task definition parameters are not valid on Fargate, gpu among them.
EC2: the unit is an instance. You choose the instance type, own the OS, and scale with an Auto Scaling group, which keeps the instance count between a minimum and a maximum and launches or terminates instances through scaling policies. Auto Scaling itself has no fee; you pay for the instances, busy or idle. In return you get every instance type, including GPUs, and full control.
Watch it run
The animation follows upload events into an SQS queue. Lambda's poller reads it and invokes the function with batches. The first batch finds no environment, so Lambda builds one: a cold start. A burst brings two more cold starts, and reserved concurrency of three caps the function while the rest waits in the queue. A finished environment takes the next batch warm, a failing message ends in the dead-letter queue, and provisioned concurrency closes the scene.
Lambda & Event-Driven Design
Step 1 of 11
Upload events land in a queue. Lambda runs a poller that reads the queue for you and invokes your function with batches.
The same interactive animation as the lesson — step through it with the controls.
The code
What you hand to each service, illustrative; the ARN, subnet and security group IDs are placeholders. A function with the maximum timeout, a Fargate service of two tasks, and an Auto Scaling group of instances:
aws lambda create-function --function-name resize --runtime python3.12 \
--handler app.handler --zip-file fileb://app.zip \
--role arn:aws:iam::123456789012:role/resize --timeout 900 --memory-size 1024
aws ecs create-service --cluster prod --service-name api --launch-type FARGATE \
--task-definition api:7 --desired-count 2 \
--network-configuration "awsvpcConfiguration={subnets=[subnet-0abc],securityGroups=[sg-0abc]}"
aws autoscaling create-auto-scaling-group --auto-scaling-group-name gpu-workers \
--launch-template LaunchTemplateName=gpu-node --min-size 1 --max-size 4 \
--vpc-zone-identifier subnet-0abc
Now a toy model, not AWS, of cost and latency per minute over a day. The unit prices are illustrative inputs, relative numbers chosen only so the options differ; the shape of the result is the point. Lambda pays for what runs and counts cold starts; Fargate and EC2 pay for what is provisioned, scale a few minutes late, and count minutes when demand exceeded capacity:
import math
# ILLUSTRATIVE inputs, not AWS prices: the cost of one unit of capacity
# (think one vCPU) for one minute, relative to EC2 = 1.0.
PRICE = {"Lambda": 3.0, "Fargate": 1.6, "EC2": 1.0}
SCALE = { # (units per task or instance, minimum count, minutes to react)
"Fargate": (1, 1, 3),
"EC2": (8, 1, 5),
}
TARGET = 0.7 # tasks and instances keep 30% headroom
KEEP_WARM = 10 # toy: minutes an idle Lambda environment stays warm
def run(option, demand, price=PRICE):
"""Toy model, not AWS. demand[t] = busy capacity units in minute t.
Returns (cost, idle share of what was paid for, slow events)."""
cost = paid = used = slow = 0
warm = []
for t, d in enumerate(demand):
if option == "Lambda":
need = math.ceil(d)
slow += max(0, need - max(warm[-KEEP_WARM:], default=0)) # cold starts
warm.append(need)
cap = d # billed only while code runs
else:
size, minimum, lag = SCALE[option]
seen = demand[max(0, t - lag)] # autoscaling reacts late
cap = size * max(minimum, math.ceil(seen / TARGET / size))
slow += d > cap # minutes over capacity
cost += cap * price[option]
paid += cap
used += min(d, cap)
return round(cost), round(100 * (1 - used / paid)), slow
def profile(busy_minutes, peak=40.0, base=0.2):
"""A day of minutes: `peak` for the first busy_minutes of every hour."""
return [peak if t % 60 < busy_minutes else base for t in range(1440)]
bursty = profile(2) # 2-minute spikes, hourly
steady = [20 + 2 * math.sin(t / 90) for t in range(1440)]
for name, demand in (("bursty", bursty), ("steady", steady)):
results = {opt: run(opt, demand) for opt in PRICE}
for opt, (cost, idle, slow) in results.items():
unit = "cold starts" if opt == "Lambda" else "minutes over capacity"
print(f"{name} {opt:8} cost={cost:6} idle={idle:2}% {slow} {unit}")
print(" cheapest:", min(results, key=lambda o: results[o][0]))
# bursty Lambda cost= 6595 idle= 0% 937 cold starts
# bursty Fargate cost= 6955 idle=91% 46 minutes over capacity
# bursty EC2 cost= 14488 idle=95% 46 minutes over capacity
# cheapest: Lambda
# steady Lambda cost= 87458 idle= 0% 28 cold starts
# steady Fargate cost= 67802 idle=31% 0 minutes over capacity
# steady EC2 cost= 46080 idle=37% 0 minutes over capacity
# cheapest: EC2
On the bursty day Lambda is cheapest despite many cold starts, because tasks and instances sit 91% and 95% idle. On the steady day EC2 keeps its instances busy and wins. Now the claim that matters, across three illustrative Lambda prices and seven levels of busy minutes per hour. L, F and E mark the cheapest option:
busy = (1, 2, 5, 10, 20, 40, 60) # busy minutes per hour
print("Lambda price x", " ".join(f"{b:>2}" for b in busy))
for lam in (2.0, 3.0, 5.0): # three illustrative prices
price = dict(PRICE, Lambda=lam)
row = []
for b in busy:
costs = {o: run(o, profile(b), price)[0] for o in price}
row.append(min(costs, key=costs.get)[0]) # L, F or E
print(f"{lam:14}", " ".join(f"{c:>2}" for c in row))
# Lambda price x 1 2 5 10 20 40 60
# 2.0 L L L L E E E
# 3.0 L L F F E E E
# 5.0 F F F F E E E
The prices move the boundary, but not the direction. In every row the winner moves from Lambda towards EC2 as utilisation rises, and never back.
The complexity
The real costs, beyond the bill:
- Lambda: cold starts on new environments, the 15-minute ceiling, and a Regional concurrency quota shared by every function. Long jobs need to be split, or moved to containers.
- Fargate: service auto scaling adds whole tasks in response to metrics, not to each request, so capacity keeps headroom. No GPUs.
- EC2: the OS, patching and scaling policies are yours. Idle instances bill, so utilisation is the whole game; discounts for steady use are in AWS cost optimization.
Where it goes wrong
- Lambda for a long or steady job. A job over 15 minutes fails; a function busy all day often costs more than a container that would be busy all day.
- Fargate for GPU work. Not supported; GPU tasks need EC2 capacity.
- EC2 for a few requests an hour. You pay for an instance that is idle almost all the time.
- Ignoring Lambda's concurrency quota. One hot function can throttle the others in the Region; see Lambda cold starts and concurrency.
- Comparing list prices instead of utilisation. The cheapest unit price loses if most of the units sit idle.
When it shows up in interviews
It appears as "design an image-processing pipeline", "where does this API run?" or "why is our serverless bill so high?". A strong answer asks about traffic shape and run time first, then picks: Lambda behind a queue for events, Fargate for the always-on API, EC2 for GPU inference or a licensed database. How an Auto Scaling group keeps instances healthy is in EC2 Auto Scaling.
How to say it in an interview
"I'd decide by the unit of deployment and the traffic shape. Lambda runs a function per request with nothing to manage, up to 15 minutes per invocation, billed only while it runs, so it wins for spiky, event-driven work, with cold starts as the cost. Fargate runs container tasks without instances to manage and has no run-time ceiling, so it suits long-running services, but you pay for the task's size while it runs. EC2 gives full control and every instance type, including GPUs, and wins when I can keep instances busy. Between them, utilisation decides the cheapest, not the unit price."
The queue-in-front pattern, partial batch failures and concurrency are in Lambda and event-driven design.
Frequently asked questions
When should I use Lambda instead of Fargate?
Use Lambda for event-driven or spiky work that finishes within 15 minutes per invocation: queue consumers, file processing, webhooks. You pay only while code runs and scale per request. Use Fargate for long-running services, steady traffic, or anything that needs a process that stays up.
Is Fargate cheaper than EC2?
It depends on utilisation. Fargate bills for the task's CPU and memory while it runs, with no instances to manage. EC2 bills for the instance, busy or idle. If you keep instances well packed and busy, EC2 is usually cheaper per unit of work; with uneven load or little operations time, Fargate often wins.
What is the maximum run time of a Lambda function?
15 minutes, or 900 seconds, per invocation, the function timeout quota in the Lambda documentation. Lambda Managed Instances allow longer runs for asynchronous and most event source mapping invocations. Longer work belongs in a container on Fargate or EC2, or split into steps.
Sources
- Lambda quotas — AWS Documentation
- Understanding the Lambda execution environment lifecycle — AWS Documentation
- Understanding Lambda function scaling — AWS Documentation
- Architect for AWS Fargate for Amazon ECS — AWS Documentation
- Amazon ECS task definition differences for Fargate — AWS Documentation
- Automatically scale your Amazon ECS service — AWS Documentation
- What is Amazon EC2 Auto Scaling? — AWS Documentation