Most AWS bills I audit are not bloated because of exotic architecture mistakes. They are bloated because of a dozen boring defaults nobody revisited: on-demand pricing for steady workloads, gp2 volumes, log groups that never expire, S3 buckets with no lifecycle rules, NAT gateways carrying traffic that should never touch them. Below are 20 fixes, grouped by where the money leaks, each with a realistic savings range, a worked example, and — where it helps — the exact CLI command or config. Individual line items routinely drop 50–90%. Your total bill won't fall by the sum of those percentages, but a 25–40% reduction in the first quarter is a normal outcome.

I spend a lot of my time on cloud cost reviews. Before touching anything, we plot every candidate optimization on a simple 2×2: effort to implement on one axis, savings potential on the other.
The pattern is remarkably stable across companies. The top-left quadrant — low effort, good savings — is almost never empty. It's usually full. Teams skip it because these changes feel too simple to matter, or because nobody owns "the bill" as a product.
This article is that quadrant, written down. I first published a version of it for the Ukrainian DevOps community; this is a rewrite for a global audience, updated for what has changed in AWS pricing since: the public IPv4 charge, Graviton5, Database Savings Plans, and the end of shared commitment discounts through resellers.
One rule before we start: measure first. Turn on Cost Explorer with resource-level granularity, activate cost allocation tags, and open Cost Optimization Hub. Every fix below should show up as a line you can watch move.

Savings potential: up to 72% vs On-Demand
If an EC2, Fargate or Lambda workload has been running at a predictable floor for the last three months, paying On-Demand for that floor is a choice — usually an unexamined one.
AWS offers three compute-side Savings Plans:
m8g in eu-central-1), any size within it. Deepest discount: up to 72%.You commit to a dollar-per-hour spend for one or three years, paid all upfront, partially upfront, or with no upfront payment.
What it looks like in practice. For an m8g.large in us-east-1 at time of writing, On-Demand is about $0.090/hour. A one-year Compute Savings Plan brings that to roughly $0.066 (~27% off); a three-year EC2 Instance Savings Plan to about $0.041 (~55% off).
How to size it without regret:
For many companies — especially ones where a one-year plan already is the long-term plan — a one-year, no-upfront Compute Savings Plan is the right default. Less discount, almost no regret.
Savings potential: up to 72% (RIs), up to 35% (Database Savings Plans)
Compute Savings Plans don't cover managed databases, caches or search clusters. Historically, that's where Reserved Instances still mattered: RDS, ElastiCache, OpenSearch and Redshift all have their own reservation models.
The newer option is Database Savings Plans, announced at re:Invent 2025. They work like Compute Savings Plans for the data layer: a one-year $/hour commitment, no upfront payment, covering Aurora, RDS, DynamoDB, ElastiCache, DocumentDB, Neptune, Keyspaces, Timestream and DMS — both provisioned and serverless usage — for up to 35% off.
Rule of thumb: if your database fleet is stable in engine and instance class, classic RDS Reserved Instances still give the deeper discount. If you expect to change engines, move to Aurora Serverless, or shift spend between DynamoDB and RDS over the year, the flexibility of a Database Savings Plan is usually worth the lower ceiling.

Savings potential: typically 20–40% on price-performance
Graviton is AWS's own ARM server CPU line. Per hour, a Graviton instance is usually cheaper than its x86 counterpart of the same size; per unit of work, the gap tends to be larger, because each vCPU is a full physical core rather than a hyperthread.
The generation that matters right now: AWS announced Graviton5 at re:Invent 2025, debuting in M9g instances with up to 25% more performance than the previous generation, 192 cores per chip and a much larger cache. Graviton4 (M8g, C8g, R8g) is the broadly available workhorse.
Where it's nearly free to switch:
Where it takes real work: native dependencies without ARM builds, old JDKs, closed-source agents. Audit those first.
bash
# Build and push a multi-architecture image once; run it anywhere
docker buildx build --platform linux/amd64,linux/arm64 \
-t <account>.dkr.ecr.<region>.amazonaws.com/api:1.4.2 --push .
Savings potential: up to 90% vs On-Demand
Spot Instances are spare EC2 capacity sold at a steep discount. The trade-off: AWS can reclaim them with a two-minute warning.
A correction to a myth I still hear regularly: Spot is no longer an auction. Since 2017, Spot prices move gradually based on long-term supply and demand, and you don't need to bid. You may set a maximum price, but by default it's capped at the On-Demand price, and you pay the current Spot price, not your maximum. At time of writing, m8g.large Spot in us-east-1 was running around 60% below On-Demand.
Good fits: CI/CD runners, batch and data processing, stateless web tiers behind a load balancer, dev/test environments, Kubernetes worker nodes for stateless pods.
Bad fits: single-instance databases, anything that can't checkpoint, long uninterruptible jobs.
Make it reliable:
price-capacity-optimized allocation strategy).Savings potential: often 30–50% of node spend in underutilized clusters
The most common EKS cost problem I see: nodes running at 20–30% utilization because nobody consolidates them after traffic drops.
There are three approaches, and the right one depends on how spiky your load is:
A minimal Karpenter NodePool that prefers Spot, allows Graviton and consolidates aggressively:
yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["arm64", "amd64"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
Whatever you pick, set realistic resource requests. Inflated requests defeat every autoscaler.
Savings potential: 5–20%, sometimes more
AWS keeps several generations of each family available. Older ones are rarely cheaper per hour and are almost always worse per unit of work.
General-purpose large instances, On-Demand, us-east-1, Linux:
|
Instance |
CPU |
Generation |
$/hour |
|---|---|---|---|
|
m4.large |
Intel |
Legacy |
$0.100 |
|
m5.large |
Intel |
Previous |
$0.096 |
|
m6g.large |
Graviton2 |
Previous |
$0.077 |
|
m7g.large |
Graviton3 |
Current |
$0.0816 |
|
m8g.large |
Graviton4 |
Current |
$0.0898 |
Two lessons hide in this table. First, moving from m5 to m6g cuts the hourly price by about 20% for the same nominal size. Second, the newest generation is not the cheapest per hour — m8g costs more than m6g. It typically does more work per hour, so you may run fewer or smaller instances. Benchmark your own workload; decide on cost per request or per job, not per hour.
Savings potential: frequently the single largest source of quick wins
AWS Compute Optimizer analyzes utilization metrics and flags over- and under-provisioned resources: EC2 instances, Auto Scaling groups, EBS volumes, Lambda functions, ECS services on Fargate, RDS databases, and idle resources. It tells you, for example, that a Lambda function has three times the memory it ever uses, or that a Fargate task requests 2 vCPU and peaks at 0.3.
Cost Optimization Hub sits on top: it consolidates recommendations from Compute Optimizer, Savings Plans and Reserved Instance analysis, and idle-resource detection across all accounts in your organization, deduplicates them, and estimates savings in dollars.
Two practical tips:
Make reviewing the hub a recurring calendar event with an owner. A recommendation nobody reads saves nothing.


Savings potential: 40–95% on data that goes cold
Most S3 buckets are written once and read rarely after the first few weeks — logs, backups, user uploads, data lake partitions. Leaving them in S3 Standard means paying hot-storage prices for cold data.
S3 Intelligent-Tiering moves each object between access tiers based on its own access pattern, automatically and without retrieval fees:
|
Tier |
Moves there after |
Price per GB-month (us-east-1) |
|---|---|---|
|
Frequent Access |
— |
$0.023 (same as Standard) |
|
Infrequent Access |
30 days without access |
$0.0125 (~45% less) |
|
Archive Instant Access |
90 days without access |
$0.004 (~83% less) |
|
Archive / Deep Archive Access (opt-in) |
90–180+ days |
lower still, asynchronous retrieval |
If an object is accessed again, it moves back to Frequent Access. The cost is a monitoring fee of $0.0025 per 1,000 objects per month; objects smaller than 128 KB are not monitored and always billed at the Frequent Access rate.
Worked example. 100 TB in S3 Standard costs about $2,300 per month. Suppose that after a few months 20% is hot, 30% has gone 30+ days untouched, and 50% hasn't been read in 90+ days:
Total: about $1,060 per month — roughly 54% less, with zero application changes.
Skip it for buckets dominated by tiny objects, and for data with known, fixed lifetimes — use lifecycle rules (next item) instead.
Savings potential: depends on volume; often substantial on old accounts
Intelligent-Tiering handles unknown access patterns. Lifecycle rules handle known ones: "CI artifacts expire after 30 days", "audit logs go to Glacier Deep Archive after 90 days and are deleted after seven years".
Things people forget in S3 lifecycle rules:
AbortIncompleteMultipartUpload after 7 days to every bucket.ECR is the same story with Docker images. Every CI build pushes a new image; nothing deletes them. A sensible default policy:
json
{
"rules": [
{
"rulePriority": 1,
"description": "Expire untagged images after 14 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 14
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Keep only the last 30 tagged images",
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["*"],
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": { "type": "expire" }
}
]
}
Savings potential: 20% per GB, more if you were over-provisioning for IOPS
gp3 is the current general-purpose SSD. In us-east-1 it costs $0.08 per GB-month versus $0.10 for gp2, and it ships with a 3,000 IOPS and 125 MB/s baseline regardless of size. With gp2, IOPS scale with size — so teams often provisioned 1 TB volumes just to get IOPS for a 200 GB database. With gp3, you buy size and performance separately.
Example: 5 TB of gp2 across an account costs about $500 a month; the same capacity on gp3 costs about $400. That's $100 a month for one command per volume, with no downtime:
bash
# List all gp2 volumes in the current region
aws ec2 describe-volumes \
--filters Name=volume-type,Values=gp2 \
--query 'Volumes[].VolumeId' --output text
# Convert one in place (online, no detach required)
aws ec2 modify-volume --volume-id vol-0123456789abcdef0 --volume-type gp3
Also update your launch templates, Terraform modules and Kubernetes StorageClasses so new volumes are gp3 by default. And while you're in there, delete unattached volumes and snapshots older than your retention policy.
Savings potential: modest directly; meaningful through faster scaling
Smaller images cost less in ECR storage, need less EBS on every node, transfer less data across AZs and through NAT gateways, and — the biggest effect — start faster, so autoscaling reacts sooner and you can run with less headroom.
The techniques that matter most:
distroless, alpine, or scratch for static binaries..dockerignore so .git, node_modules and test data never enter the build context.&& and clean caches in the same RUN step.dockerfile
# Build stage
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api
# Runtime stage: a few MB instead of ~800 MB
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/api /api
ENTRYPOINT ["/api"]

Savings potential: depends on traffic; often hundreds to thousands per month
Here's a line item that shocks people the first time they find it. A private subnet reaching S3, DynamoDB, ECR or SQS goes through a NAT gateway by default, and the NAT gateway charges $0.045 per GB processed (us-east-1) on top of its hourly fee. Pulling 10 TB a month from S3 through a NAT gateway costs about $450 in processing alone — for traffic that never leaves AWS.
There are two kinds of VPC endpoint, and the difference matters:
The math for interface endpoints: each one runs about $7 per AZ per month. In a three-AZ VPC, that's around $22 a month, which breaks even against NAT processing at roughly 600 GB a month of traffic to that service. ECR image pulls on a busy EKS cluster clear that bar easily; a lightly used Secrets Manager endpoint might not.
Savings potential: $3.65 per address per month, multiplied by however many you have
Since 1 February 2024, AWS charges $0.005 per hour for every public IPv4 address — attached or not, on EC2, load balancers, NAT gateways, RDS, everything. That's about $3.65 per address per month.
A hundred addresses is $365 a month, or $4,380 a year, for something that used to be free. Accounts that grew organically often have far more than they realize: one public IP per EC2 instance "for SSH", Elastic IPs left behind after a migration, public RDS instances.
What to do:
bash
# Find Elastic IPs that aren't associated with anything (pure waste)
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' \
--output table
Savings potential: grows with the size of your public footprint
Public IPv6 addresses carry no hourly charge. For new VPCs, dual-stack is now the sensible default; for public-facing services, ALBs, NLBs and CloudFront all support IPv6 clients.
Be realistic about the scope. Some AWS services and many third-party APIs still require IPv4, so most workloads will be dual-stack for years. The near-term saving comes from item 13. IPv6 is the structural fix that stops the IPv4 bill from growing with you.
Savings potential: frequently one or two instance sizes
AWS default parameter groups are tuned for a generic, middle-of-the-road workload. If RDS is a large share of your bill, the cheapest instance upgrade is often the one you avoid by tuning.
Where I usually find the money:
db.r7g, db.r8g) for memory-hungry databases; db.m7g / db.m8g for balanced loads. Moving from an older Intel class to Graviton is typically a 10–20% saving by itself.gp3 is now available for RDS and lets you provision IOPS separately from size. Many databases on Provisioned IOPS (io1/io2) don't actually need it — check the real IOPS in CloudWatch before paying for them.pg_stat_statements or Performance Insights will usually show you three queries that account for most of the load. Fixing those can let you drop a size.Savings potential: indirect; lets you run smaller databases
RDS Proxy pools and reuses database connections instead of letting every application process or Lambda invocation open its own. It also makes failovers faster and more transparent to the application.
Picture a web app on RDS with spiky traffic — a promo campaign, a product launch. Connection storms, not queries, are what push the database over: memory fills up with connection overhead, latency climbs, and the reflex is to buy a bigger instance. RDS Proxy absorbs the spike and keeps the connection count to the database flat.
An honest note: RDS Proxy is not free. It's billed per vCPU of the underlying database. It pays for itself when it lets you avoid an instance-size increase or stops serverless functions from exhausting connections. For a monolith with a well-configured connection pool, it may add cost without saving any.

Savings potential: often substantial, and it compounds
New CloudWatch log groups default to "Never expire". Storage is $0.03 per GB-month — cheap per gigabyte, but it accumulates every month forever.
Ingest 1 TB of logs a month with no retention, and after a year you're storing 12 TB: about $360 a month in storage alone, and climbing. With a 30-day retention policy, you're storing roughly 1 TB — about $30.
Two more levers:
bash
# Find log groups with no retention policy...
aws logs describe-log-groups \
--query 'logGroups[?!retentionInDays].logGroupName' --output text
# ...and set one
aws logs put-retention-policy \
--log-group-name /aws/lambda/my-function --retention-in-days 30
Pick retention per log type based on compliance requirements — not "never" by default.
Savings potential: depends on resource churn; can be significant in Kubernetes-heavy accounts
AWS Config records configuration changes and evaluates them against rules. It's valuable for compliance and security. It's also billed per configuration item recorded and per rule evaluation — and with "record all resource types" enabled, high-churn resources generate enormous volumes of items.
The usual culprits are resources that change constantly as part of normal operation: network interfaces and security groups on autoscaling EKS nodes, for example.
What to do:
Savings potential: hard to put a number on — but this is what keeps the other 18 savings from eroding
Every item above is a one-off fix. This one stops the waste from coming back, especially once you run workloads in several regions and many accounts.
team, environment, service and cost-center, enforce them with Tag Policies or SCPs, and activate them in Billing. Untagged spend is unowned spend.Savings potential: varies; the bigger value is avoided mistakes
One important update since I first wrote this list. As of 1 June 2025, AWS no longer allows Reserved Instances and Savings Plans to be shared across separate end customers within a reseller's organization. The old model — a partner buying commitments in bulk and spreading the discount across many clients — is gone. If a provider's main pitch is still "we'll get you a discount by pooling commitments", ask them how.
What partners still legitimately bring:
The return on expertise in cloud cost work is asymmetric. One misconfigured NAT path or one oversized commitment can cost more than an entire audit.
|
Week |
Do this |
Typical effort |
|---|---|---|
|
1 |
Turn on Cost Explorer, Cost Optimization Hub, tags, budgets and anomaly detection |
Hours |
|
1 |
Add S3/DynamoDB gateway endpoints; set CloudWatch log retention; delete unattached EIPs and volumes |
Hours |
|
2 |
Convert gp2 → gp3; add S3 and ECR lifecycle policies; enable S3 Intelligent-Tiering on large buckets |
1–2 days |
|
2–3 |
Act on Compute Optimizer rightsizing; move dev/test and CI to Spot |
Days |
|
3–4 |
Pilot Graviton on one service; buy a conservative one-year Compute Savings Plan for the stable floor |
Days |
|
Ongoing |
Karpenter/consolidation on EKS, RDS tuning, IPv6, monthly cost review |
Weeks |
Cost optimization is never a single action. It's a set of habits plus continuous control over your infrastructure.
There is no universal recipe, either. Every change on this list trades something: commitment flexibility, interruption tolerance, engineering time. So before you pull any lever, ask the same question: what does this do to the business, not just to the bill? Saving $2,000 a month by making deployments fragile is a bad trade.
But in almost every account I've reviewed, the low-effort, high-savings quadrant was full. Start there. Most of these changes take hours, not weeks.
If you want a second pair of eyes on your AWS bill, I'm happy to talk — the Gart Solutions team runs cloud cost audits and optimization engagements. And I'd genuinely like to hear in the comments which of these moved the needle most for you, or what I missed.
Author’s note: Pricing in this article is for us-east-1, Linux, On-Demand, as of October 2026 unless stated otherwise. AWS prices change and vary by region — check the official pricing pages before you make commitment decisions.
This article was published under HackerNoon's Business Blogging program.