AWS ECS vs Lambda vs EC2: Where Should You Deploy Your Backend?
Three ways to run the same API on AWS, three very different bills and on-call experiences. Here is how to choose without over-engineering it.
For most backend APIs, ECS on Fargate is the safest default; Lambda is the better choice for event-driven, spiky or low-traffic workloads; and EC2 is for when you need full control of the machine or the lowest cost at steady, high utilisation. The right answer depends on your traffic shape, how long requests run, and how much operations work your team can absorb — not on which service is newer.
Key takeaways
- Lambda: pay per request and duration, zero idle cost, scales fast, but has a 15-minute limit and cold starts.
- ECS Fargate: run containers without servers; predictable, works for any long-running process; you pay while tasks run.
- EC2: full control and potentially the lowest unit cost, but you own patching, scaling and recovery.
- Steady traffic favours containers or instances; bursty and event-driven work favours Lambda.
- Containerise early — it keeps the move from Fargate to EC2 (or elsewhere) cheap later.
The three options in one paragraph each
AWS Lambda
You upload a function (zip or container image), AWS runs it in response to events — an API Gateway request, an SQS message, an S3 upload, a schedule. Each execution environment handles one request at a time; AWS adds environments as traffic grows and removes them when it falls. Maximum run time is 15 minutes.
Amazon ECS (on Fargate or EC2)
You package your service as a Docker image and ECS keeps a desired number of tasks running behind a load balancer, replacing unhealthy ones and scaling on metrics. With Fargate, AWS manages the underlying hosts; with the EC2 launch type, tasks run on instances you provide. ECS is simpler than Kubernetes (EKS) and is enough for most teams.
Amazon EC2
Virtual machines. You choose the instance type, install the runtime, deploy the app (often with Docker anyway), and wire up Auto Scaling groups, AMIs, patching and monitoring yourself. Maximum flexibility, maximum responsibility.
Cost model
The billing units are different, which is why "which is cheapest?" has no single answer.
- Lambda charges per request plus GB-seconds of execution time. Idle costs nothing; a function busy around the clock costs more than the equivalent container.
- Fargate charges per vCPU-hour and GB-hour for as long as tasks run. Fargate Spot and Compute Savings Plans reduce this for suitable workloads.
- EC2 charges per instance-hour. Reserved Instances, Savings Plans and Spot can cut this substantially, but only if you keep instances well utilised.
A rough rule of thumb (an estimate, not a benchmark): if your API is busy only a small fraction of the day, Lambda is typically cheapest. As sustained utilisation climbs, containers catch up and then win, and well-utilised reserved EC2 is usually the lowest raw unit cost. Don't forget the hidden line items — NAT gateways, load balancers, API Gateway requests, CloudWatch logs and data transfer often matter as much as compute. Plug your real numbers into the AWS Pricing Calculator before deciding.
Scaling behaviour
- Lambda scales per request with almost no configuration, subject to account concurrency limits. The risk is scaling too well: a traffic spike can open hundreds of database connections. Use RDS Proxy or a serverless-friendly database, and set reserved concurrency to protect downstream systems.
- ECS scales by adding tasks based on CPU, memory, request count or queue depth. New tasks typically take tens of seconds to a couple of minutes to become healthy, so keep headroom or scale on leading indicators.
- EC2 scales with Auto Scaling groups; new instances take longer to boot unless you bake AMIs or use warm pools.
Cold starts and latency
A cold start happens when Lambda has to create a new execution environment: download code, start the runtime, run your initialisation. For small Node.js or Python functions this is often a few hundred milliseconds; for large Java or Spring Boot functions it can reach seconds without mitigation. These are typical ranges, and your package size and init code dominate.
Mitigations, in order of cost:
- Shrink the package and move heavy initialisation out of the hot path (lazy-load SDK clients you rarely use).
- Use SnapStart for Java (and other supported runtimes) to restore from a snapshot.
- Choose arm64 (Graviton) and right-size memory — more memory also means more CPU.
- Pay for provisioned concurrency on latency-critical endpoints.
ECS and EC2 have no per-request cold start; once a task is warm it stays warm. That makes them a better fit for latency-sensitive APIs with consistent p99 targets, WebSockets and long-lived connections.
Operational burden
| You manage… | Lambda | ECS Fargate | EC2 |
|---|---|---|---|
| OS patching | No | No | Yes |
| Runtime / base image updates | Runtime version only | Yes (Dockerfile) | Yes |
| Capacity planning | Concurrency limits | Task size and count | Instances, AMIs, ASGs |
| Local development parity | Weaker (emulators) | Strong (same container) | Strong |
| Debugging in production | Logs and traces only | Logs, traces, ECS Exec | Full shell access |
For a small team, operations time is usually the most expensive resource. Fargate and Lambda remove most undifferentiated server work; EC2 gives it all back to you. The DevOps side of this — pipelines, images, monitoring — is covered in DevOps for full-stack developers.
Decision table
| Workload | Best default | Why |
|---|---|---|
| REST/GraphQL API with steady traffic | ECS Fargate | No cold starts, predictable cost, any framework |
| Low-traffic or early-stage API | Lambda | Near-zero idle cost |
| Webhooks, S3/SQS/EventBridge consumers, cron jobs | Lambda | Native event integrations, scales to zero |
| WebSockets, long-lived connections, streaming | ECS Fargate | Long-running processes, stable connections |
| Jobs longer than 15 minutes | ECS tasks (or AWS Batch) | Lambda's time limit |
| Heavy Java/Spring Boot monolith | ECS Fargate | Startup time and memory suit long-running tasks |
| GPU inference, special hardware | EC2 (or ECS on EC2) | Fargate and Lambda don't offer GPUs |
| Large, steady, cost-sensitive fleet | ECS on EC2 with Savings Plans/Spot | Lowest unit cost at high utilisation |
A typical startup path
Most startups don't need to pick one forever. A path I'd recommend for a typical SaaS backend:
- MVP: one containerised API on ECS Fargate (or Lambda if traffic is truly tiny), a managed PostgreSQL database on RDS, static frontend on S3 and CloudFront. Infrastructure as code from day one.
- First customers: add Lambda for asynchronous work — emails, webhooks, file processing — fed by SQS or EventBridge. The API stays on Fargate.
- Growth: split out services only where scaling or team boundaries demand it (see microservices vs monolith), add autoscaling on request count, and buy Compute Savings Plans once usage is stable.
- Scale: move steady, heavy services to ECS on EC2 capacity providers with Spot where it saves real money; keep event-driven pieces on Lambda.
The surrounding pieces — VPC layout, databases, CI/CD and security — are covered in my guide to AWS cloud architecture for startups.
Keep the code portable
// app.ts — plain Express app, no AWS-specific code
export const app = express();
app.use(express.json());
app.get("/health", (_req, res) => res.json({ ok: true }));
app.use("/api/orders", ordersRouter);
// server.ts — entry point for ECS / EC2
app.listen(Number(process.env.PORT ?? 3000));
// lambda.ts — entry point for Lambda behind API Gateway
import serverless from "serverless-http";
export const handler = serverless(app);
Keeping the application framework-agnostic and the entry points thin means switching compute is a deployment change, not a rewrite. The same idea applies to Spring Boot with the AWS serverless Java container. For more on structuring the service itself, see the Node.js and Express API architecture guide.
FAQ
Is Lambda cheaper than ECS Fargate?
For spiky or low traffic, usually yes, because Lambda costs nothing when idle. For steady, sustained traffic, Fargate or EC2 usually becomes cheaper, because Lambda's per-request and per-millisecond pricing adds up when functions run most of the time. Model your own traffic with the AWS Pricing Calculator rather than relying on general rules.
Are Lambda cold starts still a problem?
For most Node.js and Python APIs, cold starts are typically in the low hundreds of milliseconds and affect a small share of requests. They matter more for large Java or .NET functions, functions in a VPC with heavy initialization, and latency-sensitive endpoints. SnapStart, provisioned concurrency, smaller packages and lazy initialization reduce them.
Should I use ECS on EC2 or ECS on Fargate?
Start with Fargate: no instances to patch or scale, and you pay per task. Move to ECS on EC2 capacity providers when steady usage is high enough that reserved or spot instances save meaningful money, or when you need GPUs, specific instance types or daemon workloads.
Can I run a Spring Boot or Express API on Lambda?
Yes. Express and other Node.js frameworks run on Lambda through adapters, and Spring Boot runs with the AWS serverless Java container plus SnapStart to reduce cold starts. It works well for low or spiky traffic, but long-lived connections, large frameworks and background threads fit more naturally in containers on ECS.
When does plain EC2 still make sense?
When you need full control of the machine: GPUs, specialised instance types, software that is hard to containerise, licensing tied to hosts, or steady workloads where reserved instances or savings plans give the lowest cost and your team is comfortable managing servers.
Need help choosing?
If you're deciding how to deploy a Node.js or Spring Boot backend on AWS, I can help with the architecture and the migration. See my full-stack development services or get in touch.