AWS Lambda vs EC2: Which Should Run Your Workload?
EC2 rents you a virtual machine you keep running and patch yourself; Lambda rents you a single invocation and bills only while your code runs. Pick Lambda for event-driven or spiky work that finishes inside its 900-second timeout. Pick EC2 for long-running, always-busy or hardware-specific workloads, where committed pricing wins.
The one-line difference: a unit of capacity vs a unit of work
Amazon EC2 (Elastic Compute Cloud) rents you a virtual machine. You choose an instance type (how many vCPUs and how much RAM), you choose an AMI (a disk image containing an operating system), and from the moment it boots until you stop it, everything above the hypervisor is yours: the OS, the patches, the web server, the process supervisor.
AWS Lambda rents you an invocation. You upload a handler function, tell AWS which events should trigger it, and AWS creates and destroys the machinery to run it on demand. You never see, name, SSH into or patch the thing your code runs on.
That is the whole comparison in one sentence: with EC2 you provision capacity ahead of demand; with Lambda AWS provisions capacity per request. Every difference below — limits, scaling, cold starts, the shape of the bill — falls out of that.
"Serverless" does not mean there is no server. Lambda runs your code inside Firecracker microVMs, lightweight virtual machines that give hardware-level isolation between different customers' workloads. There is a server. You just don't own it.
Note
This article compares the two ends of the spectrum. AWS Fargate and Amazon ECS sit in the middle — long-running containers without instances to manage — and are worth reaching for when neither end fits. There is a note on that at the end.
Execution model: how each one actually runs your code
On EC2 you boot an instance and start a long-lived process — a systemd service, a container, a JVM application server. That process handles many requests at once using threads or an event loop, and anything you put in memory (a connection pool, an in-process cache, a request counter) survives from one request to the next because the process never exits.
Lambda's execution environment has three phases:
| Phase | What happens | When you pay for it |
|---|---|---|
| Init | Lambda downloads your code, starts the language runtime, and runs everything outside your handler | Once per environment |
| Invoke | Your handler function runs against one event | Every invocation |
| Shutdown | The environment is torn down | Not billed as a request |
The single most important thing to internalise: one execution environment processes exactly one invocation at a time. Ten simultaneous requests do not become ten threads inside one process. They become ten separate execution environments, each with its own copy of your init code, its own memory, its own database connection.
That has teeth. An in-process cache in a Lambda gives you a cache hit rate you cannot predict, because you don't know which of the N environments took the request. A counter in a module-level variable counts a fraction of the traffic. A database connection pool of size 20 in a function running at 200 concurrency is 4,000 connections, not 20.
Three ways Lambda gets invoked
Lambda supports three invocation models, and you don't write the plumbing for any of them:
- Synchronous (
RequestResponse) — the caller waits for the return value. This is Amazon API Gateway, an Application Load Balancer, or a Lambda function URL, which gives a function its own HTTPS endpoint with either IAM auth or none. - Asynchronous (
Event) — Lambda queues the event and returns immediately. If the invocation fails, Lambda retries it twice by default with delays, and you can route the failures to a dead-letter queue or an on-failure destination. - Event source mapping — Lambda polls Amazon SQS, Kinesis or DynamoDB Streams for you and invokes your function with batches.
On EC2 the equivalent of that third one is a consumer process you write, deploy and keep alive yourself — the same design work you'd do for a Kafka consumer group, including who retries what.
Networking differs too. An EC2 instance always lives in a subnet inside your VPC. A Lambda function lives outside your VPC unless you attach it, and when you do, Lambda uses Hyperplane ENIs shared across all execution environments that use the same subnet and security group combination, rather than creating a network interface per invocation.
The limits that decide the answer for you
Before comparing anything else, check your workload against Lambda's quotas. Several of them are not negotiable, and one of them will often make the decision without you.
| Limit | Value | Adjustable? |
|---|---|---|
| Function timeout | 900 seconds (15 min) max; 3 seconds default | No |
| Memory | 128 MB – 10,240 MB, in 1 MB steps; 128 MB default | No |
Ephemeral storage (/tmp) |
512 MB default, up to 10,240 MB | Configurable |
| Synchronous payload | 6 MB request, 6 MB response | No |
| Asynchronous payload | 256 KB | No |
| Deployment package | 50 MB zipped upload, 250 MB unzipped, 10 GB container image | No |
| Concurrent executions | 1,000 per account per Region | Yes, via Service Quotas |
The 900-second ceiling is the one that ends most arguments. A nightly report that takes 40 minutes, a large video transcode, a persistent WebSocket connection, a game server — none of these fit, no matter how you configure the function.
The memory setting is subtler than it looks, because on Lambda you buy CPU by buying memory. Lambda allocates CPU proportionally: a function gets the equivalent of one full vCPU at 1,769 MB and up to 6 vCPUs at 10,240 MB. A CPU-bound function set to 128 MB is not saving you money — it is running on a sliver of a core and paying for the extra wall-clock time.
/tmp is configurable up to 10,240 MB, but it is scratch space that disappears with the environment. There is no Lambda equivalent of an EBS volume you attach, fill and keep.
Common mistake
Trying to fit a fat runtime into the 250 MB unzipped package limit by stacking layers. You can attach up to 5 layers, but the function plus all its layers still has to come in under 250 MB — layers reorganise the limit, they don't raise it. If you need more, use a container image, which goes to 10 GB.
Scaling and cold starts
An EC2 Auto Scaling group scales by launching and terminating instances against a minimum, maximum and desired capacity, usually driven by a target-tracking policy ("keep average CPU at 60%"). Adding capacity means booting an instance, warming the application and passing health checks — that is minutes, so you hold spare headroom permanently to survive a spike.
Lambda scaling needs no configuration at all. Each function can add 1,000 concurrent executions every 10 seconds, independently of other functions in the account, until it hits the account's regional concurrency limit — after which invocations are throttled with a TooManyRequestsException.
A cold start is the Init phase of a brand-new execution environment: downloading the code, starting the runtime, running your top-level initialisation. It is paid on the first invocation in that environment, not on every one. A warm start skips straight to Invoke.
What makes it worse: big deployment packages, runtimes with heavy startup work such as the JVM or .NET, and doing expensive things at module level (loading a 200 MB model, building an SDK client tree, reading secrets).
Two mitigations, with their costs stated honestly:
- Provisioned concurrency pre-initialises a number of environments so they're ready immediately, removing the Init-phase cold start for them. You are billed for the concurrency you configure and the period you configure it — whether or not a single request arrives. This quietly reinstates the always-on cost model you left EC2 to escape.
- SnapStart takes an encrypted snapshot of the initialised environment and resumes from it. AWS states it can improve startup latency by up to 10x. It supports Java, Python and .NET managed runtimes, and it cannot be combined with provisioned concurrency.
The other lever is reserved concurrency, which sets a maximum for one function and carves that capacity out of the account's shared pool. It does two jobs at once: it guarantees the function that capacity, and it stops a runaway function from consuming the whole 1,000 and starving everything else in the Region.
Cost: how the two bills are built
Lambda bills per request plus GB-seconds — configured memory multiplied by execution duration, rounded up to the nearest 1 millisecond. Requests are $0.20 per million on the on-demand x86 tier in us-east-1. Idle costs nothing. The free tier of 1 million requests and 400,000 GB-seconds per month does not expire after 12 months.
EC2 bills the instance per second with a 60-second minimum from launch to stop, whether it serves one request or a million, plus EBS storage, plus data transfer, plus a load balancer if you need one.
A worked crossover
Take an API doing 2 million requests a month, each running 200 ms at 512 MB.
- Requests: 2M, of which 1M is free → 1M billable, at $0.20 per million = $0.20.
- Compute: 512 MB is 0.5 GB. 0.5 GB × 0.2 s = 0.1 GB-seconds per request. 2M requests = 200,000 GB-seconds, entirely inside the 400,000 GB-second free tier → $0.
Two million requests, about twenty cents. But look at what is actually being consumed: 200,000 GB-seconds over a month is roughly 400,000 seconds of half-a-gigabyte compute against a month of about 2.6 million seconds — the equivalent box is busy well under 20% of the time. That is the shape Lambda is built for.
Now hold the compute constant and change the shape. Same 200,000 GB-seconds of work delivered as a service that is busy continuously rather than in bursts, and you are paying Lambda for compute you could have bought once as a running instance — and that instance can take Reserved Instance pricing at up to 72% off, Spot at up to 90% off if it tolerates a two-minute interruption notice, or a Compute Savings Plan at up to 66%. The crossover is not a request count. It is duty cycle: the closer your service runs to saturating a box around the clock, the more EC2 wins.
Two levers make Lambda cheaper without changing the architecture: arm64 (Graviton2), which is priced lower per GB-second than the default x86_64 and which AWS says can give up to 34% better price-performance; and right-sizing memory upward, because more memory means more CPU, and a function that finishes in half the time at double the memory costs the same GB-seconds while returning faster.
Warning
The line items people forget. On Lambda: CloudWatch Logs ingestion and storage, provisioned concurrency charges, API Gateway per-request cost, and a NAT Gateway if your VPC-attached function needs internet. On EC2: idle capacity you provisioned for a peak that comes twice a day, and the engineer-hours spent patching.
Note that a Compute Savings Plan applies across EC2, Fargate and Lambda, so committing does not lock you into one of them.
If you are in India, treat the us-east-1 numbers above as illustrative only. Both Lambda and EC2 pricing is Region-specific, and ap-south-1 (Mumbai) rates differ — price your own workload in the AWS Pricing Calculator before you commit to a design.
Who owns what
The AWS Shared Responsibility Model draws the line in a different place for each service. On EC2 you own guest OS patching, runtime upgrades, security group configuration, AMI hygiene and replacing sick instances. On Lambda, AWS operates the infrastructure, the OS and the platform; you own your function code, its configuration, and its IAM execution role — the identity your function assumes when it calls other AWS services.
Observability follows the same split. Lambda writes function logs to CloudWatch Logs automatically, provided the execution role grants permission to create log groups and put log events — the AWSLambdaBasicExecutionRole managed policy is exactly that. On EC2 you install and run the CloudWatch agent yourself.
Availability is worth stating plainly, because it is where single-instance deployments quietly fail their own SLO. The Amazon Compute SLA commits to 99.99% monthly uptime for EC2 at the Region level with instances across multiple Availability Zones — but only 99.5% for a single instance. One EC2 box is not a highly available deployment. Lambda is multi-AZ by default.
The decision
| Workload shape | Run it on |
|---|---|
| HTTP API with spiky or unpredictable traffic | Lambda |
| HTTP API with steady high load, near-saturated 24/7 | EC2 with a Savings Plan or RIs |
| Cron job finishing in under 15 minutes | Lambda |
| Batch job running 40 minutes | EC2, or Fargate |
| SQS / Kinesis / DynamoDB Streams consumer | Lambda (event source mapping) |
| WebSocket or game server holding connections | EC2 |
| GPU inference, or a specific instance type | EC2 |
| Needs a large persistent local disk | EC2 with EBS |
| Glue code between AWS services | Lambda |
| Licence-bound or legacy software, lift-and-shift | EC2 |
| Containerised long-running service, no instances to manage | Fargate on ECS |
Interview tip
In an interview, "Lambda is cheaper" is the wrong answer to "Lambda or EC2?". The answer that lands is duty cycle plus the hard limits: Lambda bills only while your code runs, so it wins on low and bursty utilisation and loses on a box that is busy all day; and the 900-second timeout, the 10,240 MB memory ceiling and the 6 MB synchronous payload limit disqualify some workloads outright regardless of cost.
The last thing worth saying: this is not binary. If your workload is a long-running container that doesn't fit Lambda's limits but you also don't want to patch instances, AWS Fargate on Amazon ECS gives you the container model without the VM ownership. Reach for it before you force a 20-minute job into a 15-minute box.
Frequently asked questions
- Can Lambda replace EC2 for a REST API?
- Yes, for most APIs. You put API Gateway, an Application Load Balancer or a Lambda function URL in front of the function and it scales without an Auto Scaling group. The two things to check first are whether any request or response exceeds the 6 MB synchronous payload limit, and whether your latency budget tolerates cold starts on the first invocation into each new execution environment. If it doesn't, provisioned concurrency or SnapStart addresses that, at a cost.
- What happens when a Lambda function hits the 900-second timeout?
- Lambda stops the invocation and it is recorded as an error. Nothing is checkpointed for you, so any partial work your code has done is simply abandoned unless you wrote it to durable storage yourself. For asynchronous invocations the event is retried twice by default, which means a job that always takes longer than 15 minutes will simply time out three times. Split the work into smaller units or move it to EC2 or Fargate.
- Does raising Lambda memory always cost more?
- No, and it often costs the same or less. Billing is GB-seconds — configured memory multiplied by duration — and CPU scales with memory, with one full vCPU at 1,769 MB. If doubling memory halves the execution time of a CPU-bound function, the GB-seconds are unchanged and the function returns twice as fast. Only memory you allocate but cannot use faster is wasted money.
- Is the 1,000 concurrent executions limit per function or per account?
- Per AWS account per Region, shared across every function in that Region, and it is an adjustable service quota you can raise through Service Quotas. The separate scaling rate — 1,000 additional concurrent executions every 10 seconds — applies per function. Use reserved concurrency on individual functions if you need to stop one of them consuming the whole account pool.
- Do I still need a VPC for Lambda?
- Only if the function has to reach private resources such as an RDS database or an internal service. By default a Lambda function runs outside your VPC and can reach the public internet. Attaching it to a VPC uses shared Hyperplane ENIs per subnet and security group combination, and if the function also needs outbound internet access from private subnets you will need a NAT Gateway, which is a real monthly cost line.
- What is the difference between provisioned concurrency and reserved concurrency?
- Provisioned concurrency pre-initialises execution environments so they respond without an Init-phase cold start, and you pay for that capacity for as long as it is configured. Reserved concurrency does not pre-warm anything — it sets a maximum concurrency for a function and carves that share out of the account pool, guaranteeing the function that capacity while also capping it.
References
- Amazon EC2 Reserved Instances PricingAWS
- Spot InstancesAmazon EC2 User Guide
- Lambda quotasAWS Lambda Developer Guide
- Lambda execution environmentAWS Lambda Developer Guide
- Understanding Lambda function scalingAWS Lambda Developer Guide
- Configuring provisioned concurrencyAWS Lambda Developer Guide
- Improving startup performance with Lambda SnapStartAWS Lambda Developer Guide
- Configuring Lambda function memoryAWS Lambda Developer Guide
- Configuring ephemeral storageAWS Lambda Developer Guide
- Invoking Lambda functionsAWS Lambda Developer Guide
- Asynchronous invocationAWS Lambda Developer Guide
- Networking and VPC configurationAWS Lambda Developer Guide
- Lambda instruction set architectures (arm64 and x86_64)AWS Lambda Developer Guide
- Security isolation and the Lambda execution environmentAWS Lambda Developer Guide
- AWS Lambda PricingAWS
- Amazon EC2 On-Demand PricingAWS
- Compute Savings PlansAWS
- What is Amazon EC2 Auto Scaling?Amazon EC2 Auto Scaling User Guide
- Shared Responsibility ModelAWS
- Amazon Compute Service Level AgreementAWS
- Using CloudWatch Logs with LambdaAWS Lambda Developer Guide
- Lambda function URLsAWS Lambda Developer Guide
- Managing Lambda reserved concurrencyAWS Lambda Developer Guide
- Lambda layersAWS Lambda Developer Guide