Choosing AWS Compute: EC2, ECS, EKS, Fargate and Lambda

·


The actual axis: operational burden vs control

Every AWS compute option trades control for operational burden, roughly along one line:

More control, more burden  ←――――――――――――――――――――――――――――→  Less control, less burden
EC2 (self-managed)  →  ECS on EC2  →  ECS/EKS on Fargate  →  Lambda

The question isn’t “which is more powerful” — EC2 is strictly more capable than Lambda in the sense that you can run anything on it. The question is what you’re willing to operate, and what you get back for not operating it. A team of 3 engineers running a handful of services should rarely be making the same compute choice as a 200-engineer platform org, even if the workloads look similar on paper.

EC2: full control, full operational ownership

You own the instance: OS patching, scaling policies, placement, networking down to the ENI, and recovery when an instance (or an AZ) fails. In return you get full control — any OS, any kernel module, any licensing-locked software that needs a specific host configuration, direct access for performance tuning that higher-level abstractions hide from you.

Choose EC2 directly (not via ECS/EKS) when:

  • The workload needs OS-level access or kernel-level configuration no container runtime abstracts away
  • Licensing is tied to specific hardware/host characteristics (some commercial software still works this way)
  • You’re running something that genuinely doesn’t containerize well, or legitimately doesn’t benefit from containerization (a single long-lived stateful service with no scaling story)

Don’t default to raw EC2 for a fleet of application services just because it’s the most familiar option — you’ll end up rebuilding orchestration, health checking, and deployment tooling that ECS or EKS already provide, and maintaining that tooling becomes its own project.

A decision path from workload shape to an AWS compute service START FROM THE WORKLOAD, NOT THE SERVICE Your workload Event-driven, short, bursty? Containerised already? Needs kernel/host control? Lambda 15-minute ceiling ECS / EKS on Fargate no host to patch EC2 or ECS on EC2 you own the host yes yes yes no: default to the container path ECS or EKS, on Fargate to start WHAT THE TREE DOES NOT DECIDE FOR YOU Fargate charges per allocation; EC2 lets you bin-pack, and rewards you only if you actually do. ECS versus EKS is an operating-model choice, not a capability one. A VPC-attached Lambda shares a Hyperplane ENI per subnet and security-group pair; it does not create one per concurrent execution, which is the assumption that drives bad subnet sizing. Verify current limits against AWS documentation before sizing anything. A decision path from workload shape to an AWS compute service START FROM THE WORKLOAD, NOT THE SERVICE Your workload Event-driven, short, bursty? yes: Lambda 15-minute ceiling no, next question Containerised already? yes: ECS / EKS on Fargate no host to patch no, next question Needs kernel/host control? yes: EC2 or ECS on EC2 you own the host no: default to the container path ECS or EKS, on Fargate to start WHAT THE TREE DOES NOT DECIDE FOR YOU Fargate charges per allocation; EC2 lets you bin-pack, and rewards you only if you actually do. ECS versus EKS is an operating-model choice, not a capability one. A VPC-attached Lambda shares a Hyperplane ENI per subnet and security-group pair; it does not create one per concurrent execution, which is the assumption that drives bad subnet sizing. Verify current limits against AWS documentation before sizing anything.
A path from workload shape to a service. What it deliberately does not decide is the operating-model question underneath ECS versus EKS.

ECS vs EKS: when Kubernetes is justified, and when it’s organizational overhead

ECS is AWS’s own container orchestrator — simpler to operate, tightly integrated with other AWS services (IAM roles per task, native ALB/NLB integration, CloudWatch), and a much smaller operational surface than Kubernetes. It does less than Kubernetes, which for most teams is the point, not a limitation.

EKS is managed Kubernetes — you get the Kubernetes API and its entire ecosystem (Helm charts, operators, the enormous body of existing tooling and institutional knowledge in the industry), at the cost of Kubernetes’ own operational complexity: node group management (even with managed node groups, you’re tuning them), add-on version compatibility, RBAC, and a steeper learning curve for anyone new to the platform.

ECSEKS
Control planeFully managed by AWS, no visible complexityManaged, but the Kubernetes API surface is large — you’re still operating “a Kubernetes cluster” conceptually
Learning curveLow if the team knows AWS; ECS-specific concepts (task definitions, services) are narrowSteep if the team doesn’t already know Kubernetes — and it’s a deep enough ecosystem that “knowing Kubernetes” is itself a specialization
EcosystemSmaller — mostly AWS-native toolingEnormous — most CNCF tooling, Helm charts, operators assume Kubernetes
PortabilityAWS-specific — task definitions don’t move to another cloudKubernetes manifests are largely portable to other clouds or on-prem clusters
Best fitTeams standardized on AWS with no multi-cloud requirement, who want orchestration without taking on Kubernetes operational depthTeams with existing Kubernetes expertise, a genuine multi-cloud/portability requirement, or workloads that need specific Kubernetes-ecosystem tooling (service meshes, specific operators) with no AWS-native equivalent

The honest framing: Kubernetes is justified when you need its ecosystem or its portability, not because it’s the industry-standard answer. A platform team that already runs Kubernetes elsewhere and wants one consistent operational model across clouds has a real reason to choose EKS. A team picking EKS because job postings list it, or because “everyone uses Kubernetes,” is importing a genuinely large body of operational complexity — cluster upgrades, add-on compatibility matrices, RBAC design, the fact that a Kubernetes cluster is itself a piece of infrastructure that needs its own on-call coverage — for ecosystem benefits they may never use. That complexity is organizational overhead when nothing about the workload actually required it.

Fargate vs EC2 launch type (for ECS or EKS)

Whichever orchestrator you pick, you still choose how the underlying compute is provisioned:

EC2 launch typeFargate
What you manageThe EC2 instances — patching, scaling the node fleet, bin-packing tasks onto instancesNothing below the task/pod — AWS provisions and manages the compute per task
Cost shapePay for provisioned instances regardless of task utilization — efficient if you bin-pack well, wasteful if you don’tPay per vCPU/memory-second actually allocated to the task — no bin-packing problem, but typically a higher per-unit rate than well-utilized EC2
Density/controlYou control instance type, placement, and can run multiple tasks per instance tightly packed“Each Fargate task has its own isolation boundary and does not share the underlying kernel, CPU resources, memory resources, or elastic network interface with another task” (Fargate for Amazon ECS, AWS docs) — strong isolation, but no ability to pack multiple workloads onto shared hardware for efficiency, and a subset of task definition parameters is unsupported or behaves differently
Startup characteristicsInstance-level scaling lag when new capacity is neededTask-level provisioning, generally faster to add new task capacity since there’s no instance bootstrap in the loop
Best fitSteady, predictable, high-utilization workloads where bin-packing several tasks per instance beats Fargate’s per-task overheadVariable or spiky workloads, teams that want zero node management, or workloads where the operational savings outweigh the per-unit cost premium

The crossover point is utilization: a well-bin-packed EC2 fleet running near capacity is usually cheaper per unit of compute than Fargate running the same workload, because Fargate’s isolation model means you’re not sharing a host’s slack capacity across tasks. But “well-bin-packed” requires someone actively managing that packing — if your EC2 fleet actually runs at low, uneven utilization because nobody’s tuned the bin-packing, Fargate’s “pay for what you allocate” model may already be cheaper in practice, before you even count the engineer-hours saved on node management. Measure your actual EC2 utilization before assuming EC2 is the cheaper choice — the theoretical cost advantage only holds if you’re capturing it.

Lambda: real constraints, not just “serverless”

Lambda removes server management entirely, but it comes with hard constraints that determine fit, not preference:

  • Duration limit. The function timeout maximum is 900 seconds (15 minutes). One exception now exists: functions using Lambda Managed Instances, invoked asynchronously or through an event source mapping (other than Amazon MQ and DocumentDB), support up to 5,400 seconds (90 minutes) (Lambda quotas, AWS docs). Anything that can run longer than the ceiling that applies to your invocation path, by design or under worst-case load, is not a fit for a single invocation — you need Step Functions orchestration, a different compute target, or a redesign that chunks the work. Check the current figure before designing to it; this limit has moved more than once.
  • Cold starts. The first invocation (or an invocation after scale-up, or after idle) pays an initialization penalty — more pronounced for larger deployment packages and certain runtimes. (VPC attachment used to be a major contributor here; with Hyperplane ENIs it largely is not, for the reason described below. Measure it rather than inheriting the old assumption.) For latency-sensitive synchronous request paths, cold start variance is a real user-facing problem; Provisioned Concurrency mitigates it at a direct cost (you’re paying to keep capacity warm, which erodes part of Lambda’s “pay only when invoked” cost advantage).
  • No persistent state between invocations. This is AWS’s own positioning, not an inference: “Lambda is designed for short-lived compute tasks that do not retain or rely upon state between invocations.” Each invocation is conceptually fresh — you cannot rely on in-memory state surviving between calls (execution environment reuse happens opportunistically for performance, but it’s not a contract you should design around). Anything needing session state, connection pooling continuity, or in-memory caching needs an external store (ElastiCache, DynamoDB) in the design from the start.
  • VPC networking adds overhead, but not where people think. Attaching a function to a VPC creates a Hyperplane ENI, and the cost lands at attach time rather than per invocation: while Lambda creates it the function sits in Pending and cannot be invoked, “which can take several minutes.” Thereafter, functions sharing the same subnet-and-security-group combination reuse the same Hyperplane ENI, each supporting up to 65,000 connections before Lambda scales the ENI count (Lambda VPC networking, AWS docs). Two consequences worth designing for: a function idle for 14 days has its ENI reclaimed and goes Inactive, so the next invocation fails and the function re-enters Pending; and AWS explicitly says not to rely on ENI persistence. Removing a VPC configuration can take up to 20 minutes to clean up.
  • Concurrency limits are account- and Region-level by default. The default quota is 1,000 concurrent executions, applied per Region, shared across every function in that scope unless you’ve assigned reserved or provisioned concurrency per function (Lambda quotas, AWS docs). A burst in one function can throttle an unrelated function sharing the same pool if you haven’t partitioned it. New accounts start with lower quotas than this that AWS raises automatically with usage, which is a common surprise in a first load test.

Lambda fits event-driven, bursty, short-duration workloads extremely well — exactly the shape where paying per-invocation beats paying for idle provisioned capacity. It fits poorly for long-running processes, workloads needing consistent low-latency response with no tolerance for cold-start variance and no budget for Provisioned Concurrency, or anything architected around persistent in-memory state.

Cost shape at different utilization levels

Utilization patternCheapest option, generallyWhy
Steady, high, predictable (near-constant load)EC2 (well bin-packed), Reserved/Savings Plans pricingYou’re paying for capacity you’re actually using most of the time — the fixed-cost model wins when utilization is consistently high
Variable, business-hours patternECS/EKS on Fargate, or EC2 with aggressive auto-scalingAvoids paying for idle overnight/weekend capacity without managing scale-to-zero yourself
Bursty, infrequent, event-drivenLambdaPer-invocation pricing means near-zero cost during idle periods — the model that EC2’s always-on cost structure can’t match
Spiky with occasional long sustained burstsMixed — Fargate or EC2 for the baseline, Lambda for burst absorption at the edgesAvoid forcing one model to cover both ends of a bimodal load pattern

This is directional, not a substitute for actually modeling your workload’s cost under each option — utilization patterns, request size, and duration all shift the crossover points, and AWS’s specific pricing for each service changes. Model your actual numbers before committing, and re-check as both your traffic pattern and AWS’s pricing evolve.

Migration-friendliness: how hard is it to change your mind later

This matters more than it gets credit for, because compute choices are rarely final:

  • EC2 → ECS/EKS is usually straightforward — you’re containerizing what’s likely already a well-understood application, and the underlying EC2 knowledge transfers.
  • EC2 launch type → Fargate (within ECS/EKS) is often a configuration-level change if the application is already containerized and doesn’t depend on host-level access — one of the easier migrations in this list.
  • ECS → EKS (or the reverse) is a real rewrite — task definitions and Kubernetes manifests are not interchangeable, and this migration carries roughly the cost of adopting a new orchestrator from scratch.
  • Anything → Lambda requires genuine application restructuring — statelessness, handler-based entry points, and designing around the duration and cold-start constraints aren’t retrofits, they’re a different programming model.
  • Lambda → anything else is easier than the reverse in one sense (you already have small, decomposed units of logic) but still requires rebuilding the invocation, scaling, and deployment model around the new target.

If you’re genuinely unsure which compute model fits and expect to revisit the decision, favor the container path (ECS or EKS, on Fargate initially) over committing early to either raw EC2 or Lambda — it preserves the most options to shift launch type or orchestrator later without a full rewrite.

Related


More in this reference