You go looking for "ECS cost" in Cost Explorer and the ECS service line is nearly empty. That's not a mistake — it's how ECS bills. Filter by service and the real spend is sitting under EC2 and AWS Fargate instead, split across whatever launch type each of your services uses, with nothing under "Elastic Container Service" to tell you it's related. Nobody sized ECS itself, because there's nothing to size — which is exactly why the spend behind it can drift for months before anyone traces it back to a specific task definition or cluster.
What does ECS actually charge you for?
Nothing, on its own. AWS's own pricing page is explicit: there is no additional charge for Amazon ECS orchestration. You pay for the compute your tasks run on and nothing else — the scheduler, the API, the console, the deployment machinery are all included at no extra cost, the same way EC2's hypervisor overhead isn't billed back to you as a separate line.
What you actually pay for depends entirely on launch type, and the two types bill in structurally different ways:
- EC2 launch type — your tasks run on EC2 instances you registered as container instances in the cluster. Those instances bill exactly like any other EC2 fleet, by the instance-hour, whether or not the tasks on them are using the capacity.
- Fargate launch type — AWS runs each task on infrastructure you never see, and bills per task, by the vCPU-second and GB-second the task actually reserves.
Both are legitimate ways to run ECS. They fail differently, though, which is the part worth understanding before you go looking for the waste.
How does the EC2 launch type actually bill me?
On the EC2 launch type, the bill is the underlying instance fleet's bill, full stop. A c5.xlarge container instance costs the same whether it's running eight densely-packed tasks or one task using a tenth of its capacity — the meter is wall-clock instance time, not task usage, exactly like an EC2 instance with no ECS in the picture at all.
That means the waste on EC2 launch type shows up as slack between what the cluster reserves and what the instances can hold — tasks whose CPU and memory reservations don't add up to fill the instances they're scheduled onto, so you're paying for idle capacity between them. ECS publishes cluster-level metrics for exactly this:
# What share of the cluster's registered capacity is actually reserved by running tasks
aws cloudwatch get-metric-statistics \
--namespace AWS/ECS \
--metric-name MemoryReservation \
--dimensions Name=ClusterName,Value=my-ecs-cluster \
--start-time "$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 86400 \
--statistics Average \
--region us-east-1
CPUReservation and MemoryReservation report the percentage of the cluster's total registered capacity that task definitions have reserved — not how busy the tasks actually are, just how much of the box their reservations claim. A cluster sitting at 40% reservation most of the day has instances sized for headroom it isn't using, and every one of those instances is billing its full instance-hour rate regardless.
How does the Fargate launch type bill differently?
Fargate removes the instance from the picture, but it doesn't remove the waste — it just moves where the waste shows up. At standard us-east-1 on-demand rates for Linux/X86_64 tasks:
| What you pay for | Rate |
|---|---|
| vCPU | $0.04048 per vCPU-hour |
| Memory | $0.004446 per GB-hour |
| Fargate Spot | up to 70% off the on-demand rate |
These are standard us-east-1 on-demand rates at the time of writing. Rates vary by region and
CPU architecture, and AWS changes prices — check the Fargate pricing
page before you commit to a number.
On Fargate, you pay for exactly what a task's CPU and memory reservation says, for exactly as long as the task runs — there's no shared instance to under-fill, because AWS provisions the box per task. That sounds like it should eliminate bin-packing waste entirely, and for the instance half it does. But it makes reservation sizing matter more, not less, because now every unused unit of a reservation is billed directly with no other task around to absorb it.
Run the arithmetic on one task, continuously running for a 730-hour month, reserving 1 vCPU and 2 GB:
- vCPU: 1 × $0.04048 × 730 = $29.55
- Memory: 2 × $0.004446 × 730 = $6.49
- Total: $36.04/month
Now the same task, right-sized to what it actually uses after a look at its metrics — 0.5 vCPU and 1 GB:
- vCPU: 0.5 × $0.04048 × 730 = $14.78
- Memory: 1 × $0.004446 × 730 = $3.25
- Total: $18.02/month
Same workload, same 730 hours of runtime, and the reservation alone is the entire difference — $18.02/month per task, doubled if the reservation is doubled. On the EC2 launch type that same over-reservation might be invisible, absorbed into slack on a shared instance. On Fargate it's a line-item difference between two numbers you can compute from the task definition alone.
How do I check whether my ECS spend is going somewhere useful?
Start with what the tasks are actually using, not what they reserve. ECS publishes per-service CPUUtilization and MemoryUtilization under the same namespace, and they measure against the reservation as 100%, so a service sitting at 20% is using a fifth of what it's paying for regardless of launch type:
aws cloudwatch get-metric-statistics \
--namespace AWS/ECS \
--metric-name MemoryUtilization \
--dimensions Name=ClusterName,Value=my-ecs-cluster Name=ServiceName,Value=checkout \
--start-time "$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 86400 \
--statistics Average Maximum \
--region us-east-1
Read both statistics. The average tells you the steady-state reservation you're paying for and not using; the maximum tells you how much headroom the service genuinely needs during its busiest moments, which is the number a right-sized reservation actually has to clear. A service averaging 20% with a maximum of 35% has real room to come down; one averaging 20% with occasional spikes to 95% needs the headroom it has, and the fix is somewhere else.
For the EC2 launch type specifically, pull the cluster-level CPUReservation and MemoryReservation the same way as above, alongside the container instances' own CPUUtilization — a cluster reserving 80% of its capacity but only using 30% of it is carrying instances sized for reservations nobody is actually running hot.
How do I fix it without breaking anything?
Lowering a task definition's CPU or memory reservation isn't a live edit — it's a new task definition revision and a service deployment that replaces running tasks. Cut a reservation below what the task needs at its actual peak and you'll see OOM kills or CPU throttling under load, not a savings line. Roll the change out to one service first and watch its utilization metrics for a few days before doing the rest of the fleet.
Right-size task definitions from the utilization data, not a guess. Set CPU and memory reservations to cover the measured maximum plus real headroom, not the number that felt safe when the service was first defined.
On the EC2 launch type, pack tasks tighter before you resize instances. The binpack placement strategy schedules new tasks onto the container instance with the least available CPU or memory first, filling instances before starting new ones, instead of spread's default of spreading tasks evenly and leaving every instance half-empty. Pair it with ECS Capacity Providers and managed scaling so the underlying Auto Scaling group grows and shrinks with actual reservation, not with a fixed instance count somebody picked once.
On Fargate, move interruption-tolerant tasks to Fargate Spot. Batch jobs, queue workers, and anything that can restart cleanly after a reclaim get up to 70% off the on-demand rate for exactly the same vCPU-second and GB-second meter.
Scale non-production services down outside business hours. A staging or dev cluster running the same reservations as production around the clock is paying full price for capacity nobody's testing against at 2 a.m., on either launch type.
What else should I check while I'm in here?
If you're running the EC2 launch type, the instances underneath your cluster are ordinary EC2 instances once you get past the ECS agent — they accumulate the same waste any EC2 fleet does, independent of how well the tasks scheduled onto them are packed. A container instance idling at low utilization because the cluster shrank and nobody resized the ASG is an idle EC2 instance; one running a larger type than the tasks on it actually require is an oversized EC2 instance for the same reason a bare EC2 fleet would be.
If ECS itself is still an open question rather than a settled one, ECS vs EKS: which one costs less to operate breaks down the control-plane fee EKS adds on top of this exact compute math, and what does an EKS cluster actually cost works the same launch-type accounting through Kubernetes' extra layer for the comparison.
More broadly, a container platform's launch-type math is one piece of a much longer bill. The service-by-service pass through the rest of an AWS bill works through the fuller list in order of savings per minute spent.
How do I keep ECS spend from drifting back up?
Checking one cluster's reservation-versus-utilization gap by hand is an afternoon. Doing it for every cluster, every service, and every container instance behind them — after every deploy that ships a new task definition with reservations nobody revisited — is the part that quietly stops happening once launch week is over. That's what a scan is for. Parsivex checks every region, separates genuinely idle EC2 instances from merely oversized ones, and keeps watching after the cleanup, so a cluster that was tightly packed at launch doesn't drift back into slack six months later with nobody noticing. For what these findings mean once they show up in your report, see Idle EC2 instance and Oversized EC2 instance.