AWS Cost Optimization: Where to Look First
Most AWS cost work fails because people start in the wrong place. They open Cost Explorer, sort by largest service, and begin resizing EC2 instances - which is the slowest, riskiest, least rewarding move available. The bill is rarely large because your instances are slightly too big. It is large because of things nobody is looking at: traffic crossing a boundary that charges per gigabyte, storage that outlived the thing it belonged to, and managed services billing for capacity that was provisioned once and never revisited.
This page is the order I work in on a paid audit, cheapest and safest first. Each step says what it costs you to get it wrong, so you can decide what to do this week and what needs a planning cycle.
Short answer: work in order of risk, not size. First delete what nothing uses (unattached volumes, orphaned snapshots, idle load balancers, unbounded log retention). Then fix data transfer, storage lifecycle and over-provisioned managed services. Rightsize compute next, with real utilisation data. Buy Savings Plans and Reserved Instances last, once the workload has stopped changing shape.
The five things an AWS bill is actually made of
Before any tactic, you need a model of where money goes. Every line on an AWS invoice falls into one of five buckets, and they behave completely differently:
- Compute - EC2, Fargate, Lambda. Elastic, so it responds to rightsizing and purchase commitments. Usually the biggest number and almost never the biggest opportunity.
- Storage - S3, EBS, snapshots, backups. Grows monotonically unless something deletes it. The most common home for pure waste.
- Data transfer - egress, cross-AZ, NAT processing. Charged per gigabyte, invisible in most dashboards, and frequently the single largest avoidable item.
- Managed-service overhead - provisioned capacity, per-cluster fees, log ingestion, idle endpoints. You pay whether or not anything uses it.
- Waste - resources attached to nothing. Unattached volumes, idle load balancers, orphaned IPs, snapshots of instances that were terminated two years ago.
Waste and data transfer are where the fast money is. Compute is where the large money is, but extracting it takes testing and risk. That ordering is the whole point of this page.
Start here: the zero-risk pass
These changes cannot break production, because they delete or downgrade things nothing is using. Do them before you touch anything with a running workload attached.
- Unattached EBS volumes. Every terminated instance that didn't have "delete on termination" set left its root volume behind, and it bills at full rate forever.
- Orphaned snapshots. Snapshot chains outlive their volumes. Nothing cleans them up by default.
- Idle Elastic IPs. An allocated, unassociated address bills hourly.
- Load balancers with no healthy targets. A deleted service often leaves its ALB running.
- Old EBS volume types. gp2 to gp3 is a per-gigabyte price cut with no performance loss at typical IOPS. It is a one-line change and it is not reversible-risky.
- Log groups with no retention policy. CloudWatch defaults to "never expire." Setting retention on historical log groups is the single cheapest storage win on most accounts.
A useful property of this pass: because none of it is load-bearing, you can do all of it in an afternoon and measure the result cleanly. Nothing else in cost optimization has that property.
The diagnostic order, and why it isn't instance types
After the zero-risk pass, work in this sequence. The logic is expected-saving divided by blast radius.
- Data transfer. Find out what is crossing a charged boundary and why. This is first because the fixes are usually configuration, not architecture, and the amounts surprise people. See the data transfer and egress breakdown and the NAT Gateway cost guide.
- Storage lifecycle. Decide how long things should live, then enforce it with policy rather than intention. Covered per-service below, plus snapshot pricing.
- Managed-service provisioning. Capacity set once and never revisited: DynamoDB tables, ElastiCache nodes, over-provisioned RDS. See RDS cost optimization.
- Compute rightsizing. Now, with real utilisation data. See EC2 rightsizing and, for Kubernetes, EKS cost optimization ordered by risk.
- Purchase commitments. Savings Plans and Reserved Instances last, once the shape of your workload has stopped moving.
Step 5 goes last for a specific reason, and it is the most expensive mistake in this field.
The Savings Plan trap. A commitment is a bet that your usage shape stays roughly constant for one or three years. Teams routinely buy a plan, then refactor the workload three months later - move to Graviton, move to Fargate, consolidate clusters - and discover they are now paying for a commitment that no longer matches anything they run. The discount was real; the bet was wrong. Rightsize and restructure first, let the architecture settle, then commit to the shape that is left. Buying commitments early feels like progress and is frequently a net loss.
The mistakes that create most of the waste
Across audits the same handful of patterns account for the majority of recoverable spend. None of them look dramatic in isolation.
- Routing AWS-service traffic through NAT. S3 and DynamoDB traffic from a private subnet goes through the NAT Gateway and pays per-gigabyte processing - when a VPC Gateway Endpoint would carry it for free. This is the most common large avoidable item I find.
- No tagging, therefore no attribution. If you cannot say what a cost belongs to, you cannot assign anyone to reduce it. Tagging is not bureaucracy; it is the prerequisite for every other step.
- Treating dev like prod. Non-production environments running 168 hours a week to serve roughly 40 hours of actual use.
- Setting requests equal to limits in Kubernetes. Widely repeated as a best practice; it destroys bin-packing and silently raises node count.
- Logging everything, forever. Ingestion is charged per gigabyte and storage accrues. Most teams discover their observability bill rivals their compute bill.
- Optimising the number that is easiest to see. Compute is legible, so it gets the attention, while transfer and storage quietly compound.
Where the money hides, service by service
One mechanism, one thing to measure, and one limit per service. These are starting points for a diagnosis, not exhaustive guides.
S3
Mechanism: objects stay in Standard long after anyone reads them, because nothing moves them. Lifecycle rules transition by age; Intelligent-Tiering does it by observed access and is the right default when access patterns are genuinely unknown.
Measure: the ratio of bytes stored to bytes retrieved per bucket per month. A bucket with high storage and near-zero GETs is a lifecycle candidate.
Limit: transitions cost money per object. A bucket of millions of tiny objects can cost more to transition than you save - and Glacier tiers carry minimum storage durations plus retrieval latency, so they are wrong for anything on a user-facing path. Also check for incomplete multipart uploads; they bill and are invisible in the console's size figure.
EBS
Mechanism: volumes and their snapshot chains outlive the instances they served. gp3 decouples IOPS from capacity, so you stop buying size to get throughput.
Measure: unattached volume count, and provisioned IOPS against actual.
Limit: snapshots are incremental, so deleting one rarely frees what its size suggests - blocks shared with other snapshots in the chain survive. Plan snapshot cleanup by chain, not by individual snapshot.
Lambda
Mechanism: billing is memory multiplied by duration, and the two are coupled - more memory means proportionally more CPU, so a function can get cheaper when you raise memory because it finishes sooner.
Measure: billed duration against max memory used, per function. Tune the product, never memory alone.
Limit: provisioned concurrency bills continuously whether invoked or not. It is correct for latency-critical paths and pure waste everywhere else.
CloudWatch
Mechanism: ingestion is charged per gigabyte and dominates; storage and custom metrics follow. Most of the volume is debug logging nobody has read.
Measure: ingested gigabytes per log group per month, and custom-metric cardinality.
Limit: retention cuts storage but not ingestion. To reduce ingestion you have to log less at source - sample, or raise the level. High-cardinality custom metrics are the other common surprise; each unique dimension combination is a separate billable metric.
CloudFront
Mechanism: the point of a CDN is that cache hits do not touch your origin. A low hit ratio means you are paying CloudFront and origin egress.
Measure: cache hit ratio per behaviour. Below roughly 80% on static assets, something is wrong - usually cache keys including unnecessary query strings, cookies, or headers.
Limit: price classes restrict which edge locations serve your traffic. Cheaper, but worse latency in excluded regions - a product decision, not a cost one.
DynamoDB
Mechanism: provisioned capacity bills whether used or not; on-demand bills per request at a premium. The right choice depends entirely on traffic predictability.
Measure: consumed against provisioned capacity, plus the storage and write amplification of every global secondary index.
Limit: each GSI duplicates the data it projects and charges its own writes - unused indexes are a recurring multiplier, not a one-off. On-demand is cheaper than badly-provisioned capacity and more expensive than well-provisioned capacity; make the switch on measured variance, not on principle.
Variable traffic: on-demand is usually the cheaper choice. Provisioned auto scaling reacts in minutes, not seconds, so spiky tables end up provisioned for the peak to avoid throttling - paying for idle capacity most of the day. AWS halved on-demand throughput prices in November 2024, so on-demand now wins on spiky or unpredictable tables far more often than older guides suggest. Provisioned with auto scaling pays off when traffic follows a smooth, predictable curve. The DynamoDB on-demand vs provisioned calculator shows where the break-even sits for your own traffic.
Large, steady workloads: provisioned capacity plus reserved capacity (one- or three-year commitments) is the cheapest way to run a big DynamoDB table with a stable baseline. If storage, not throughput, dominates the bill, move the table to the Standard-Infrequent Access table class, which trades cheaper storage for pricier reads and writes. Expire old items with TTL - TTL deletes consume no write capacity - and project only the attributes each GSI actually needs.
ElastiCache
Mechanism: node-hour billing on capacity sized for a peak that may no longer exist, often with replicas nothing reads.
Measure: memory used against allocated, eviction rate, and hit ratio.
Limit: a cache with a poor hit ratio is not too small - it is caching the wrong things, and making it bigger just raises the bill. Fix what you cache before you buy more nodes. Replicas exist for availability; if you are not reading from them, you are buying availability you should price deliberately.
Fargate
Mechanism: you pay for requested vCPU and memory for the task's full lifetime, not for what it uses. Requests are therefore the entire cost lever.
Measure: requested against actual utilisation per task definition.
Limit: Fargate's premium over EC2 buys away node management. Below a certain steady-state scale that premium is worth it; above it, managed node groups win. Switching for cost alone and inheriting node operations is often a bad trade - decide on total cost including your time.
Support plans
Mechanism: support is billed as a percentage of spend, so it scales with the bill and is easy to over-buy.
Measure: cases opened per quarter, and how many genuinely needed faster than Developer-tier response.
Limit: Business tier buys 24/7 production support and API access to Trusted Advisor. If you run production on AWS, going below it to save a percentage point is usually false economy. This is a line item to right-size once, not to optimise repeatedly.
How to tell whether it worked
Cost work without measurement turns into folklore. Two things to know before you draw conclusions:
The bill lags. Cost and Usage Report data settles over roughly 24-48 hours. A change made this morning will not show properly today, and judging it from a partial day produces a wrong answer in both directions.
Unblended and amortized disagree, and people optimise the wrong one. Unblended shows what was charged on the day; amortized spreads commitment costs across the period they cover. If you hold Savings Plans, unblended makes the month you bought them look terrible and subsequent months look artificially cheap. Pick amortized for judging efficiency, unblended for reconciling an invoice, and never compare one to the other.
The metric that actually matters. Total spend is the wrong headline number for a SaaS business, because it should grow. Track cost per tenant or cost per request instead. That ratio tells you whether infrastructure is scaling sub-linearly with revenue, which is the only question your board is really asking. A bill that rose 20% while cost per tenant fell 15% is a success, and total-spend dashboards will report it as a failure. See AWS cost optimization for SaaS for the margin framing, and what FinOps actually reports for the governance side.
For three of the items above you can get a number before changing anything: the NAT Gateway cost calculator, the snapshot cost calculator and the DynamoDB capacity calculator take your own figures.
When not to do any of this
Cost optimization has a floor, and advice in this field almost never mentions it.
Rightsizing stops paying. Once a fleet is running below roughly 15% idle, further squeezing trades reliability for small sums. You are no longer removing waste; you are removing headroom, and the first incident costs more than a year of the saving.
Pre-product-market-fit, this is usually the wrong work. If your AWS bill is $2,000/month, a heroic 40% reduction returns $800 - roughly a day of engineering time. Spending a sprint on it to save that is a bad trade, and the architecture will change underneath you anyway. Set a budget alarm, do the zero-risk pass, and get back to the product. Cost discipline becomes worth real effort when spend is material against revenue, or when you are heading into a raise and unit economics are about to be examined - see AWS cost optimization before fundraising.
Do not consolidate what is separate on purpose. Clusters and accounts are often split for blast radius or compliance isolation. Merging them shows up as a saving and as a risk you did not price. Cost is one axis, not the only one.
What I actually do on an audit
The order above is the method, and the method is the deliverable. Concretely:
- Pull the Cost and Usage Report and attribute every line to a team, service, or environment. Whatever cannot be attributed goes on a list - unattributed spend is almost always waste.
- Map data-transfer paths before touching anything else, because the fixes are configuration and the amounts are usually the surprise.
- Separate findings into zero-risk, needs-testing, and needs-architecture-change, and hand over the first group as something the team can ship that week.
- Instrument cost per tenant or per request, so the next conversation is about efficiency rather than about the invoice.
- Only then discuss commitments, against the workload shape that is left.
On a 35-node Kubernetes cluster this sequence cut 45% of monthly spend - and the large items were not instance types. They were capacity provisioned for a peak that no longer existed, storage with no lifecycle, and traffic crossing a charged boundary that did not need to.
Related engagements are written up in the portfolio, including an infrastructure modernization where cost and reliability work were the same project.
If you want this done on your account rather than reading about it: AWS cost optimization consulting. The first pass is the diagnostic order above, and the zero-risk findings usually cover the engagement.
Sources
- AWS Cost and Usage Reports - line-item billing data used for attribution
- AWS Savings Plans user guide - commitment terms and coverage
- VPC gateway endpoints for S3 and DynamoDB
- S3 Intelligent-Tiering
- AWS Compute Optimizer - rightsizing recommendations
- Cost allocation tags
Tags:
Need help with this?
I offer hands-on consulting on exactly this — aws cost optimization consulting .