AWS Cost Optimization for DevOps
Get an AWS bill under control the DevOps way: visibility with Cost Explorer, budgets and tags, then the real levers - right-sizing, scheduling, commitments, storage lifecycle, and the sneaky data-transfer costs.
Cloud bills balloon for a boring reason: it is trivial to launch things and nobody is responsible for turning them off. Every instance, volume, snapshot, and NAT gateway keeps billing by the hour whether or not anyone uses it, and the meter runs silently in the background while your team ships features. Cost optimization is not a heroic quarterly cleanup - it is a habit, and a very DevOps one: make spend visible, attribute it to owners, kill waste, and commit to the baseline you actually need. This guide walks the levers in priority order, from "you cannot fix what you cannot see" to the specific line items that quietly eat six figures. If you are new to the platform, read AWS fundamentals first; this assumes you know what EC2, S3, and a VPC are.
Visibility first: you cannot cut what you cannot see
Before you touch a single instance, get the bill onto a screen. Optimizing blind means guessing, and guessing at cloud cost is how you spend a week saving forty dollars while a forgotten data-transfer charge grows.
Cost Explorer is the first stop. It is free, it is in the console, and it lets you slice the bill by service, by account, by region, by tag, and over time. The most useful move is to group by service for the last three months and just look: usually two or three services (EC2, RDS, and data transfer are the usual suspects) are most of the bill, and everything else is noise. Optimize the top of that list and ignore the long tail. You can also break down by usage type to see why a service costs what it does - "EC2-Other" hiding a fortune in NAT gateway and EBS is a classic surprise.
For a raw, queryable view, enable the Cost and Usage Report (CUR) - a detailed line-item export to S3 that you can point Athena or QuickSight at. You do not need this on day one, but it is what you graduate to when Cost Explorer's granularity runs out and you want to run real SQL against every billed hour.
Budgets and anomaly alerts: get told before the bill does
Visibility you have to remember to check is visibility you will forget to check. The fix is to make AWS tell you when something is wrong.
AWS Budgets lets you set a monthly spend target and get alerted at thresholds - say, an email or SNS notification at 80% and 100% of a $5,000 budget, plus a forecasted alert when AWS projects you will blow past it. Set one budget for the whole account at minimum. Better: one per team or environment, so the alert lands on the person who can actually do something about it.
Cost Anomaly Detection is the smarter companion. It learns your normal spend pattern per service and flags sudden deviations - a test that spun up a GPU fleet and never tore it down, a runaway Lambda loop, a region someone left resources in. It catches the spikes a fixed budget threshold misses because they happen mid-month. Both are free. Turn them on before you do anything else in this guide; they are the smoke detectors that stop the next surprise.
Cost-allocation tags: make spend attributable
Here is the thing that makes all of the above actually work: a bill you cannot attribute is a bill nobody owns. If you cannot answer "which team, which service, which environment does this $12,000 belong to," you cannot ask anyone to cut it.
Cost-allocation tags solve this. You tag resources with a consistent scheme - team, env, service, owner - and then activate those tags in the Billing console so they show up as grouping dimensions in Cost Explorer and the CUR. Now "the bill" becomes "team-a's prod spend is up 30% this month" and it lands on a real person.
team: platform
env: prod
service: checkout-api
owner: jane@example.com
The hard part is not choosing tags, it is enforcing them. Retrofitting tags across a live account is miserable, so bake tagging into the thing that creates resources: default tags in your Terraform/CloudFormation, and tag policies in AWS Organizations to require them. A env: prod tag on every resource is also the switch that makes the scheduling and lifecycle rules later in this guide possible - you cannot "turn off non-prod at night" if you cannot tell what non-prod is.
Right-sizing: stop paying for capacity you never use
With visibility in place, go after the biggest lever first: over-provisioned compute. Teams routinely pick an instance size by guessing high "to be safe," then never revisit it. The result is a fleet running at 10% CPU, billed at 100%.
AWS Compute Optimizer does the analysis for you. It looks at CloudWatch metrics for your EC2 instances, Auto Scaling groups, EBS volumes, and Lambda functions, and recommends a right-sized target - often "this m5.2xlarge should be an m5.large" with the projected saving. Start there, validate against your own peak-load metrics (Compute Optimizer defaults to 14 days; make sure that window includes your busy periods), and step instances down.
The same logic applies to RDS, which is frequently the most over-provisioned thing in the account because nobody wants to touch the database. Check CPUUtilization, FreeableMemory, and connection counts; a db.r5.2xlarge sitting at 8% CPU is money on fire. Modifying an RDS instance class usually needs a short maintenance window, so it feels scarier than it is - but it is often the single biggest saving available.
Two adjacent wins while you are here: switch eligible workloads to Graviton (ARM) instances, which are typically cheaper for the same performance, and delete the instances nobody will admit to owning. Right-sizing is unglamorous and it is usually where the largest chunk of savings actually lives.
Turn off what nobody uses at night
Production has to run around the clock. Development, staging, QA, and CI runners almost never do - yet by default they bill 168 hours a week when they are used maybe 50. Turning non-prod off outside working hours is close to free money, and it is a pure DevOps automation problem.
The math is blunt: a non-prod environment running only 12 hours on weekdays instead of 24/7 costs roughly 35% of the always-on price. Roll in weekends and you are near a 70% cut on that environment.
AWS Instance Scheduler is the packaged solution - a CloudFormation-deployed tool that starts and stops EC2 and RDS instances on a schedule you define with tags (Schedule: office-hours). If you prefer to own it, a tiny EventBridge rule triggering a Lambda that stops tagged non-prod instances at 8pm and starts them at 7am does the same job. This is exactly why the env tag from earlier matters: the schedule keys off it. Whatever you pick, the point is that idle non-prod compute overnight is the easiest large saving most teams are leaving on the table.
Commit to your baseline: Savings Plans, RIs, and Spot
Everything so far removes waste. This lever is about paying less for the compute you genuinely need, by trading flexibility for a discount. There are three tools and they map cleanly to three kinds of workload.
Savings Plans are the modern default for steady-state usage. You commit to a certain dollar-per-hour of compute spend for 1 or 3 years, and in exchange you pay 30-70% less than on-demand. Compute Savings Plans are the flexible flavor - the discount follows you across instance families, sizes, regions, and even between EC2, Fargate, and Lambda - so they are forgiving if your architecture shifts. Commit to the baseline you are confident you will always run (look at the flat floor in your Cost Explorer usage graph), not your peak.
Reserved Instances (RIs) are the older mechanism, still the right tool for services that do not offer Savings Plans - notably RDS, ElastiCache, and Redshift. Same idea: commit for 1 or 3 years, pay much less. For those databases, an RI on your steady baseline is one of the highest-confidence savings you can buy.
Spot Instances are the opposite trade: up to 90% off on-demand, but AWS can reclaim the capacity with two minutes' notice. That makes Spot perfect for interruptible work - CI/CD runners, batch jobs, data processing, stateless workers behind a queue, and Kubernetes node groups that tolerate churn. Never put a stateful primary database or a job that cannot restart on pure Spot.
The clean mental model: Savings Plans/RIs for the steady baseline, Spot for the interruptible spikes, on-demand for the unpredictable middle. Cover the floor with commitments, burst on Spot, and only pay full on-demand price for the part you genuinely cannot predict.
Storage lifecycle: data you forgot you were paying for
Storage costs creep because data accumulates and nobody deletes it. Unlike compute, it is not one big obvious line - it is a thousand small ones that never stop.
For S3, the win is tiering and expiry. Most objects are hot for a while and then almost never read again, so you pay Standard prices forever for data that could be a fraction of the cost. Lifecycle rules automate the move: transition objects to Infrequent Access after 30 days, to Glacier after 90, and expire them entirely after your retention period. If your access pattern is unpredictable, S3 Intelligent-Tiering does the same thing automatically for a small monitoring fee, and is a safe default for large unknown buckets. Also enable expiry for incomplete multipart uploads - failed uploads leave orphaned parts that bill silently forever.
For EBS, the waste is two-fold. First, unattached volumes: terminate an instance without "delete on termination" and its volumes live on, billing for gigabytes nobody can reach. Second, old snapshots: automated backups pile up indefinitely unless something prunes them. Use Data Lifecycle Manager (DLM) to age out snapshots on a policy, and periodically hunt for available (unattached) volumes and delete them.
# Find unattached EBS volumes - each one is billing for nothing
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query "Volumes[].{ID:VolumeId,Size:Size,AZ:AvailabilityZone}" \
--output table
None of these are large individually. Together, forgotten storage is often 10-20% of a mature account's bill, and lifecycle rules make the cleanup permanent instead of a chore you redo every quarter.
The sneaky ones: data transfer, idle load balancers, and logs
These are the charges that surprise people because they do not map to a resource you launched on purpose. They hide inside "EC2-Other" and "data transfer" on the bill, and they are where a scary amount of money can leak.
NAT gateways are the classic. A NAT gateway charges both an hourly fee and a per-GB processing fee on everything flowing through it. Push a lot of traffic from private subnets out to the internet - or worse, to S3 and DynamoDB through the NAT instead of a VPC endpoint - and the per-GB charge dwarfs the hourly one. Add gateway VPC endpoints for S3 and DynamoDB (they are free) so that traffic skips the NAT entirely; it is often the single highest-ROI network fix available.
Cross-AZ and egress data transfer is the other silent tax. Traffic between availability zones is billed per GB in both directions, so a chatty microservice mesh spread across three AZs can rack up real cost just talking to itself. Data leaving AWS to the internet (egress) is billed too, and it is not cheap at volume - front heavy egress with CloudFront, which has lower per-GB rates and cuts origin load. Keep chatty services AZ-aware where latency and cost matter.
Idle load balancers bill by the hour whether or not anything is behind them. Left-over ALBs and NLBs from deleted environments are pure waste - hunt for load balancers with zero healthy targets and delete them.
Log retention is the quiet one. CloudWatch Logs defaults to never expire, so every log group grows forever at ingestion-plus-storage rates. Set a retention period on every log group (30 or 90 days for most things), and for logs you must keep long-term, export to S3 with a lifecycle rule instead of paying CloudWatch storage prices indefinitely.
Unit cost is the real health metric
Total spend going up is not automatically bad - if you doubled revenue, doubling the bill might be fine. The number that actually tells you whether things are healthy is unit cost: cost per request, per customer, per transaction, per active user.
A rising total bill with falling unit cost means you are scaling efficiently and winning. A flat total bill with rising unit cost means waste is creeping in even though nothing looks alarming on the top-line. Absolute dollars hide both stories. Pick a denominator that matches your business (cost per thousand requests, cost per paying customer) and track the bill divided by it over time. It reframes every optimization in this guide from "spend less" to "get more per dollar," which is the framing that survives growth. Cost-allocation tags are what make this measurable per service.
Build cost into design, not cleanup
The cheapest optimization is the architecture you never build wrong. Once the account is under control, the durable move is to shift cost left - make it a first-class concern in design reviews, the same way you already treat security and reliability.
That means asking the cost question early: does this need a NAT gateway or a VPC endpoint? Does this service really need to run 24/7 or can non-prod sleep? Is this workload interruptible enough for Spot? What is the retention policy for this data before we start generating it? A one-line question in a design doc costs nothing; discovering the answer on next month's bill costs a cleanup project. Give someone visible ownership of the bill (or rotate it), review the top movers in Cost Explorer regularly, and treat a cost anomaly like any other alert.
The shape of it
Cost optimization is a loop, not a project. First get visibility - Cost Explorer to see the bill, Budgets and Anomaly Detection to get warned, and cost-allocation tags so every dollar has an owner. Then pull the levers in order of impact: right-size over-provisioned EC2 and RDS, schedule non-prod to sleep at night, commit to your baseline with Savings Plans and RIs while running interruptible work on Spot, and put storage on lifecycle rules so old data and orphaned volumes stop billing. Then go after the sneaky line items - NAT gateways and cross-AZ data transfer, idle load balancers, and unbounded log retention - because that is where the surprise costs hide. Track unit cost so you know whether you are actually getting more per dollar, and push cost thinking into design reviews so you stop creating the waste in the first place. Do the loop continuously and the bill stays a number you understand instead of one that scares you.