AWS Cost Optimization for a Dockerised Stack: Right-Sizing ECS, Fargate Spot, and Egress Traps
Right-size ECS, use Fargate Spot, avoid egress traps. Production checklist from 25+ years of SaaS infrastructure decisions.
I've spent 25+ years building and running SaaS products, and if there's one lesson that repeats across every stack I've touched, it's this: nobody notices the AWS bill creeping up until finance forwards you the invoice with a red circle drawn around it. At Sumo Group, working on infrastructure that supports a marketplace at real scale, cost isn't an afterthought — it's a design constraint, same as latency or availability. This isn't a "turn off things you don't need" listicle. It's the checklist I work through when a Dockerised AWS stack starts costing more than it should.
AWS Pricing Components: What You're Actually Paying For
Before optimizing anything, you need a mental model of where the money goes. AWS pricing breaks down into three buckets:
- Compute costs — EC2 instances, Fargate tasks, Lambda invocations. Billed by time (or by request/duration for serverless).
- Storage costs — S3, EBS volumes, snapshots, RDS storage. Billed by GB-month, with additional charges for requests and retrieval.
- Networking costs — Data Transfer Out, cross-AZ traffic, NAT Gateway processing. This is the bucket everyone underestimates.
For containerised stacks, compute is the visible cost, but networking is the silent drain. A well-architected ECS cluster with efficient task sizing can still bleed money through NAT Gateway charges or cross-AZ chatter between services that nobody bothered to co-locate.
Underneath those three buckets, most Dockerised-stack bills break down further into a predictable shortlist: ECS/Fargate or EC2 compute, ECR image storage, ALB/NLB hours plus LCU charges, NAT Gateway hourly + per-GB processing, Data Transfer Out, and whatever RDS/ElastiCache/DynamoDB you're running behind the app. If you can't explain each line item on last month's invoice in one sentence, that's the actual starting point — not a rightsizing spreadsheet.
Pricing Models: On-Demand, Reserved Instances, Savings Plans, and Spot
AWS gives you four fundamentally different ways to pay for the same compute, and picking the wrong one is the single most common source of waste I see.
On-Demand is the default — pay per second, no commitment, most expensive per unit. Use it for spiky, unpredictable workloads and anything you're still validating in production.
Reserved Instances (RIs) commit you to a specific instance type in a specific region for one or three years in exchange for a steep discount. RIs make sense for genuinely stable baseline load — the always-on database replica, the API gateway that never drops below a certain floor. RIs are largely an EC2/RDS concept; if your compute is entirely Fargate, Savings Plans are the relevant lever, not RIs.
Savings Plans are the more flexible evolution of RIs — you commit to a dollar-per-hour spend rather than a specific instance type, and AWS applies the discount across EC2, Fargate, and Lambda automatically. For a mixed containerised stack, a Compute Savings Plan is usually better than instance-specific RIs, because it doesn't lock you into a family you might outgrow.
Spot Instances let you use spare AWS capacity at up to 90% off On-Demand pricing (AWS's own published ceiling — real-world discounts vary by instance family, region, and time, and typically land well below that maximum but still substantial), with the trade-off that AWS can reclaim that capacity with two minutes' notice. Fargate Spot inherits the same mechanism and is genuinely interesting for Docker workloads — more below.
My rule of thumb: baseline load goes on Savings Plans, bursty/stateless load goes on Spot, and experimental work stays On-Demand until its usage pattern stabilizes. Mixing all three within a single ECS cluster, task-by-task, is normal and correct.
Right-Sizing ECS and Fargate
Most Dockerised stacks lose money quietly here. Teams containerise an app, pick a task CPU/memory size that "feels safe," ship it, and never revisit it. Six months later you've got fifty tasks provisioned at 2 vCPU / 4GB each, running at 15% average utilization.
Start with AWS Compute Optimizer. It analyzes CloudWatch metrics for your ECS services (and EC2 instances, and Lambda functions) and gives you concrete recommendations — this task definition is over-provisioned, this EC2 instance family is a poor fit, this Lambda memory allocation is wasting money. It's free and already running against your account if enabled.
Practical right-sizing checklist for ECS/Fargate:
- Pull actual CPU/memory utilization for each service over a 2–4 week window, not a snapshot. Container workloads have daily and weekly cycles.
- Compare against Compute Optimizer's findings. It flags underutilized instances and over-sized task definitions with specific alternatives.
- Downsize task CPU/memory incrementally. Fargate bills by exact CPU/memory combination, so shaving a task from 1 vCPU/2GB to 0.5 vCPU/1GB is a real, immediate saving multiplied by every running copy.
- If you're on EC2-backed ECS, revisit instance family choice. A compute-heavy workload on a general-purpose instance pays for unused memory; a memory-heavy one on compute-optimized is the inverse mistake. Compute Optimizer flags this directly.
- Watch task count vs. task size. It's often cheaper to run more small tasks that scale elastically than fewer large tasks sized for peak load that sit idle most of the day.
A quick sanity check that catches most over-provisioning without any tooling at all: open CloudWatch, look at CPUUtilization and MemoryUtilization for the service, and ask whether either metric is regularly clearing 60–70% at peak. If neither ever does, the task definition is oversized — full stop, no further analysis needed to justify a first pass at downsizing.
Fargate Spot: The Underused Lever
Fargate Spot deserves its own callout because it's well-suited to containerised, stateless services — which describes a large chunk of what people run on ECS. If your service is horizontally scaled, stateless, and tolerant of a task being killed and rescheduled, Fargate Spot gives you Spot's discount without managing Spot fleets or bidding logic yourself — ECS handles capacity requests and interruption behind the capacity provider abstraction.
Recommended pattern: run baseline capacity on standard Fargate (or Savings Plan-covered Fargate), and configure your ECS service's capacity provider strategy to burst additional tasks onto Fargate Spot during traffic peaks. You define weights and a base count across the FARGATE and FARGATE_SPOT capacity providers, and ECS handles the mix automatically — for example, a base of 2 tasks always on standard Fargate, with additional scaled-out tasks weighted toward Spot. Background job processors, async workers, and CI/build runners are near-perfect candidates — they're resilient by design and often run in short bursts.
Where Fargate Spot doesn't fit: anything stateful, anything with long-running requests that can't tolerate mid-flight interruption, and anything where two minutes' notice isn't enough time to drain gracefully.
Serverless and Containers Together
Lambda and Fargate aren't competitors — they're different tools for different shapes of work. Lambda's pay-per-use model is unbeatable for spiky, event-driven, short-duration work: webhook handlers, image processing triggers, scheduled cleanup jobs. Fargate and ECS win when you have long-running processes, need more control over networking and container images, or have workloads that don't fit Lambda's execution time and package size limits.
EKS is the third option, and cost-wise it's rarely the cheapest unless you're already running Kubernetes elsewhere. The control plane cost and tooling overhead usually aren't worth it for teams whose only requirement is "run containers cost-effectively." Default to ECS/Fargate for that and reach for EKS only when Kubernetes-specific tooling is a genuine organizational need.
Eliminating Idle Resources
This is the boring work with the highest ROI. In every account I've reviewed, there's meaningful spend on resources nobody actively uses:
- Unused EBS volumes left behind after an EC2 instance is terminated — volumes don't auto-delete unless configured.
- Old snapshots taken for a migration or one-off backup and never cleaned up.
- Idle load balancers pointing at target groups with zero healthy targets, still billing by the hour.
- Unattached Elastic IPs, forgotten NAT Gateways in decommissioned VPCs, and dev/staging RDS instances running 24/7 for environments only touched during business hours.
AWS Cost Explorer's usage breakdowns and a scheduled review of zero-activity resources will surface most of this. The discipline is reviewing on a schedule rather than only when the bill spikes.
Auto Scaling and Elasticity
Right-sizing gets you the correct baseline; Auto Scaling gets you correct behavior over time. If your ECS services and RDS-adjacent compute aren't configured to scale down during low-demand periods, you're paying peak-capacity prices around the clock for dynamic demand. Target-tracking scaling policies based on CPU or request count, combined with scheduled scaling for predictable daily patterns, close most of that gap. Trust the scaling policy instead of the static buffer.
Storage Tiering
S3 storage classes exist precisely so you stop paying Standard-tier prices for data nobody's accessed in six months. Lifecycle policies that automatically transition objects from S3 Standard to Infrequent Access, then to Glacier, are close to a free lunch — set them once, and the cost curve bends itself. On the block storage side, audit EBS volume types too; gp3 volumes let you tune IOPS and throughput independently from size rather than overpaying for capacity just to get performance.
Networking: Where Costs Hide (The Egress Traps)
Data Transfer Out is the line item most engineers can't explain when asked, and it's where a "well-optimised" Dockerised stack still bleeds money even after compute is right-sized. The traps that show up over and over:
- NAT Gateway per-GB processing charges. Every byte a private-subnet task sends to the internet (pulling a package, calling a third-party API, writing to an external webhook) passes through the NAT Gateway and gets billed per GB, on top of the hourly charge for the gateway itself. Chatty background jobs in private subnets are the usual culprit.
- Cross-AZ traffic between services that don't need geographic split. Multi-AZ is correct for resilience, but every hop between AZs — task to task, task to cache, task to database replica — is billed both ways. Co-locating latency-sensitive, high-volume internal traffic where possible, or accepting the cost consciously for redundancy, are the two honest options; "didn't realise it was cross-AZ" isn't one of them.
- Inter-region replication and cross-region calls that duplicate the NAT/DTO cost anywhere the traffic actually crosses a region boundary — easy to introduce accidentally with multi-region DR setups or misconfigured SDK endpoints.
- Serving repeat content out of origin instead of a CDN. Every asset, API response, or static file served directly out of ECS/ALB instead of through CloudFront racks up Data Transfer Out at origin rates for traffic that's identical request after request.
Consistent fixes: put CloudFront in front of anything served repeatedly to end users (CloudFront's own transfer-out pricing is generally cheaper than DTO from EC2/Fargate directly, and it absorbs repeat requests entirely), use VPC Endpoints for AWS services you call frequently from private subnets (S3, ECR, DynamoDB, Secrets Manager all support Gateway or Interface endpoints, which route traffic off the NAT Gateway entirely and often eliminate its processing charge for that traffic), use VPC Peering or Transit Gateway instead of routing inter-VPC traffic through the public internet, and be deliberate about which services actually need multi-AZ redundancy versus which are paying an AZ-crossing tax for no real benefit.
Database Cost Optimization
Databases are where "just in case" provisioning is most expensive. RDS instances sized for peak load running at 20% average utilization, DynamoDB tables left on provisioned capacity when traffic is genuinely on-demand-shaped, Redshift clusters running 24/7 for batch workloads, ElastiCache nodes oversized relative to working-set size — all common, all fixable. The provisioned-vs-on-demand decision for DynamoDB specifically deserves periodic revisits as traffic patterns mature; what was correct at launch rarely is a year later.
AWS Cost Management and Monitoring Tools
You can't optimise what you can't see, and AWS's native tooling covers most of what a solo or small team needs without buying a third-party FinOps platform:
- AWS Cost Explorer — the default starting point. Visualises historical spend by service, tag, linked account, or usage type, and forecasts near-term spend based on trend. Good for "where did the money go last month" and spotting anomalies visually.
- AWS Budgets — set a dollar or usage threshold per account, service, or tag, and get alerted (email/SNS) before you blow past it. The cheapest insurance against a runaway NAT Gateway bill or a Spot-to-On-Demand fallback that quietly doubles your compute cost.
- AWS Compute Optimizer — covered above for right-sizing; it's the tool that turns "this feels oversized" into a specific, evidence-backed recommendation with projected savings.
- Cost and Usage Report (CUR) — the most granular data AWS exposes, delivered to S3 as line-item detail. Overkill for a quick check, but it's the foundation if you want to build your own dashboards, feed a BI tool, or do proper cost allocation by tag across services.
- AWS Trusted Advisor — flags idle load balancers, underutilized EC2 instances, and a handful of other cost checks alongside its security and reliability checks; the cost checks are a useful supplement to, not a replacement for, Compute Optimizer and Cost Explorer.
For a single-founder or small-team stack, Cost Explorer plus Budgets plus Compute Optimizer covers the practical need. Third-party FinOps platforms earn their keep once you have multiple accounts, multiple teams, and cost allocation politics — not before.
Monitoring and FinOps: Making It Stick
Tooling without governance decays back into waste within a quarter. The tools above are the baseline — but the organizational layer is what keeps costs down. That means FinOps practices: consistent cost allocation tags on every resource, clear accountability per team or service for what they spend, and recurring review cadence rather than one-time cleanup. Tagging discipline sounds tedious until the month you need to answer "which team owns this $4,000 line item" and actually can.
FAQ
Is Fargate always more expensive than EC2-backed ECS? Per-unit, often yes — you're paying for the convenience of not managing instances. But once you factor in Fargate Spot for bursty workloads and the operational time saved not patching/scaling EC2 hosts, the total cost picture is frequently competitive or better for teams without dedicated infra headcount.
Should I use Savings Plans or Reserved Instances for a containerised stack? Savings Plans, in most cases. They apply across EC2, Fargate, and Lambda, which matters when your workload mix shifts over time — RIs lock you to a specific instance family and region, and don't apply to Fargate at all.
How often should I re-run right-sizing analysis? Quarterly at minimum, and immediately after any significant traffic pattern change (new feature launch, seasonal spike, major customer onboarding).
Is Fargate Spot safe for production traffic? Yes, for stateless, horizontally-scaled services that handle task interruption gracefully. Not for anything stateful or with long-running, non-resumable requests.
What's the single biggest "egress trap" for a Dockerised stack on AWS? NAT Gateway data processing charges for private-subnet tasks that call the internet frequently, closely followed by unnecessary cross-AZ traffic between services. Both are invisible until you look at the Data Transfer line items specifically — neither shows up as an obvious compute cost.
Damian Hodgkiss
Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.