Mohamed ARKID logo

Command Palette

Search for a command to run...

Command Palette

Search for a command to run...

Blog

AWS Bill Too High? 7 Fixes With Copy-Paste Terraform

AWS bill climbing? Spot, Karpenter, Graviton and NAT fixes with copy-paste Terraform. Or get a fixed-price $250 audit with results in 48 hours.

Updated 9 min read

TL;DR

If your AWS bill keeps climbing while traffic doesn't, the waste is almost always architectural, not financial. These are the seven fixes I work through on every account, roughly in order of payoff:

  1. Spot Instances for stateless and batch work.
  2. Karpenter instead of the Cluster Autoscaler.
  3. Graviton (ARM) with multi-arch images.
  4. NAT Gateway → VPC Endpoints for traffic to S3, ECR and DynamoDB.
  5. Idle non-prod shutdown outside working hours.
  6. Rightsizing instances that sit at low average CPU.
  7. Orphaned EBS volumes and snapshots cleanup.

How much these save depends entirely on where your spend sits, so measure first: Cost Explorer grouped by service and usage type shows which of the seven matter for you.


Want someone to do this on your account? Book a free 15-minute AWS bill review and I'll tell you the top 3 fixes on the call. If you want it in writing, the AWS Cost Audit is $250 fixed: written findings plus Terraform patches, delivered in 48 hours.


AWS Cost AuditWhere your money leaksThe four places waste usually hides in a startup AWS bill.
Monthly AWS Bill — 100%
Idle non-prod168 h paid

Dev and staging billed around the clock for a 40-hour work week.

NAT Gateway tollsper GB

Processing charges on traffic that never needed to leave AWS.

Over-provisioned EC2idle CPU

Instances sized for a peak that rarely comes.

Orphaned EBS / snapshotsforgotten

Unattached volumes and old snapshots nobody owns.

How big is it on your bill? The free AWS cost waste calculator estimates it in about a minute.

moarkid.com — AWS Cost Optimization

Why Your AWS Bill Is Too High

Look at your AWS dashboard right now: what is your average CPU utilization across EC2 or EKS? If it is low, you are paying for capacity you do not use.

Why? Because engineers are afraid of downtime. To survive a traffic spike, they over-provision: m5.4xlarge when an m5.xlarge would do, minReplicas: 10 when 2 is enough. Dev and staging run 24/7 for a team that works 40 hours a week. Private subnets pay NAT Gateway fees on traffic that never leaves AWS.

The usual response is a finance one: a mandate to "tag everything" and a spreadsheet review at the end of the month. Tagging is necessary for cost allocation, but it is purely reactive. By the time someone groups the tags and notices a team burned thousands on idle instances, the money is gone. Cloud cost is an engineering problem, and the fixes below are engineering fixes.

If you want a number before reading further, drop your figures into the AWS cost waste calculator. It estimates what rightsizing, Spot and NAT fixes would save on your current bill.

Compute Re-architectureSame workload, fewer idle nodesIllustrative: the shape of the change, not a measured result.

Before

Mostly idle nodes. On-Demand, fixed replicas, sized for the worst day.

→

After

Fuller, cheaper nodes. Just-in-time nodes, Spot capacity, ARM price-performance.

up to 90%
Spot: off On-Demand
up to 72%
Savings Plans: off On-Demand
up to 40%
Graviton: better price-performance

Ceilings published by AWS. What you actually save depends on your workload.

moarkid.com — AWS Cost Optimization

Fix 1: Move Stateless Workloads to Spot

Most companies run 100% of their workloads On-Demand. That is the retail price.

AWS Spot Instances sell spare capacity at up to 90% off On-Demand. The catch: AWS can reclaim the instance with a 2-minute warning.

The engineering fix: don't put your production database on Spot. But stateless microservices, background workers (Sidekiq/Celery), CI runners and batch jobs belong there. Route stateful workloads to On-Demand nodes and stateless ones to Spot, and diversify across many instance types and AZs so a single reclamation wave doesn't take out a whole service.

For the committed-spend side (Savings Plans vs Reserved Instances vs Spot, and how to layer them), see the AWS Compute Cost Playbook.

Fix 2: Replace the Cluster Autoscaler with Karpenter

The Kubernetes Cluster Autoscaler scales Auto Scaling Groups. If you need one more small pod, it adds another copy of whatever instance type the ASG was built with, even if you only need a fraction of it.

Karpenter is an open-source node provisioner. Instead of static ASGs, it watches unschedulable pods, works out the CPU and memory they need, and launches a fitting instance type directly through the EC2 API. The scheduling decision is fast (seconds); the EC2 instance still needs its normal launch and boot time before pods land on it. The win is not raw speed, it is fit: Karpenter picks from dozens of instance types and consolidates underused nodes, so you stop paying for half-empty machines.

Here is a Karpenter v1 NodePool that prefers Spot, allows both architectures, and consolidates underused nodes, with the EC2NodeClass it references:

karpenter-nodepool.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"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["5"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - alias: al2023@latest # Amazon Linux 2023
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}"

When both spot and on-demand are allowed, Karpenter prefers Spot and falls back to On-Demand when Spot capacity isn't available. Pin workloads that must never run on Spot with a nodeSelector on karpenter.sh/capacity-type: on-demand.

Fix 3: Migrate to Graviton

If everything runs on x86, you are leaving money on the table. AWS Graviton (ARM) instances are priced lower per hour than their x86 equivalents: AWS says they cost up to 20% less than comparable x86 instances. Go, Python, Node.js and Java all run on ARM, so for most services the migration is a build change: produce a multi-architecture image and let the scheduler place it on Graviton nodes.

Dockerfile

# Official Node images are published for both amd64 and arm64,
# so the same Dockerfile builds for x86 and Graviton.
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

The Dockerfile alone doesn't make the image multi-arch. The build does:

build-multi-arch.sh

# One-time: create a builder that can target multiple platforms
docker buildx create --name multiarch --use
 
# Build both architectures and push a single multi-arch manifest to ECR
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/api:1.4.0 \
  --push .

With the NodePool above allowing arm64, Karpenter can now place those pods on Graviton nodes whenever they are the cheaper fit.

Fix 4: Route Internal Traffic Around the NAT Gateway

The NAT Gateway is the classic architectural cost trap. AWS charges about $0.045 per GB processed through it, on top of the hourly fee. If private EC2 instances or EKS pods pull images from ECR or read data from S3, that traffic goes through the NAT Gateway and you pay a premium for traffic that never leaves AWS.

The engineering fix: send that traffic through VPC Endpoints instead. Gateway endpoints for S3 and DynamoDB have no hourly or per-GB charge:

vpc-endpoints.tf

# S3 traffic from private subnets bypasses the NAT Gateway
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = aws_route_table.private[*].id
}

ECR, CloudWatch Logs, STS and other services need Interface Endpoints, which have their own (much smaller) hourly and per-GB fees. The full walkthrough, including when Interface Endpoints are not worth it, is in Eliminate AWS NAT Gateway Overcharges, and the Terraform module is on GitHub: aws-vpc-endpoints-terraform.

Fix 5: Shut Down Idle Non-Prod Outside Working Hours

Your engineers work about 40 of the 168 hours in a week. Dev, staging and QA environments that run 24/7 are idle roughly three quarters of the time.

The engineering fix: a scheduled Lambda (triggered by EventBridge) that stops tagged EC2 instances, scales ECS services to zero and stops RDS instances in the evening and on weekends, then starts them before the team arrives. Resources opt in with tags, so anything that must stay up overnight simply isn't tagged.

The full Terraform and Python are in The Idle EC2 Reaper and the aws-ec2-reaper repository.

Fix 6: Rightsize Instances Sitting at Low CPU

Spot, Karpenter and Graviton change how you buy compute. Rightsizing changes how much you buy. An instance averaging 20% CPU with no memory pressure is usually one size too big, and each size down within an instance family roughly halves the price.

The engineering fix:

  • Pull at least two weeks of CPU and memory metrics (CloudWatch, or AWS Compute Optimizer, which does this analysis for you once enabled).
  • Resize toward a healthy target, around 60% average CPU, and leave headroom for spikes via autoscaling rather than via permanently oversized machines.
  • In Kubernetes, fix the pod requests first. Karpenter sizes nodes from requests, so inflated requests produce inflated nodes.

To see which resources actually drive your bill before you start, parse your Cost & Usage Report with Python and sort by cost.

Fix 7: Clean Up Orphaned EBS Volumes and Snapshots

Even with good architecture, waste accumulates. Unattached EBS volumes, old snapshots and unassociated Elastic IPs keep billing long after the instance they belonged to is gone.

Find unattached volumes with the AWS CLI:

find-orphans.sh

# EBS volumes not attached to any instance
aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[].{ID:VolumeId,SizeGiB:Size,Created:CreateTime}' \
  --output table
 
# Snapshots you own, oldest first
aws ec2 describe-snapshots --owner-ids self \
  --query 'sort_by(Snapshots,&StartTime)[].{ID:SnapshotId,SizeGiB:VolumeSize,Started:StartTime}' \
  --output table

The engineering fix: automate it. A weekly job (a Lambda, or a visual workflow in n8n) that:

  1. Lists EBS volumes with status = available.
  2. Keeps only the ones unattached for more than 14 days.
  3. Takes a final snapshot of each (just in case).
  4. Deletes the volume.
  5. Posts what it deleted, and the monthly savings, to Slack.

Apply the same idea to snapshots with an age-based retention policy (Amazon Data Lifecycle Manager handles this natively).

Keep It From Coming Back: Shift Cost Checks Left

The seven fixes cut the bill. This stops it creeping back up. Add Infracost to your CI/CD pipeline so every Terraform pull request shows its cost impact before it is merged:

.github/workflows/infracost.yml

name: Infracost
on: [pull_request]
 
jobs:
  infracost:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Infracost
        uses: infracost/actions/setup@v3
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}
 
      - name: Run Infracost
        run: infracost breakdown --path . --format json --out-file infracost.json
 
      - name: Comment on PR
        run: |
          infracost comment github --path infracost.json \
            --repo "$GITHUB_REPOSITORY" \
            --github-token "${{ github.token }}" \
            --pull-request "${{ github.event.pull_request.number }}" \
            --behavior update

If someone changes an RDS instance from db.t3.medium to db.r5.24xlarge, the PR gets a comment with the projected monthly increase. You catch it before terraform apply, not on next month's bill.

FinOps is EngineeringA loop, not a dashboardCost savings come from architecture changes — and they compound.
Repeatevery release
  1. 1Measure — parse the CUR, find the real cost drivers.
  2. 2Re-architect — rightsize, Spot, Graviton, kill idle.
  3. 3Automate — Terraform + policies so waste can't creep back.
  4. 4Repeat — every release is a chance to save more.

moarkid.com — FinOps Engineering

The Operational Reality (What Breaks)

Cost optimization is never entirely risk-free. Here is what breaks:

  • Spot reclamations. When AWS takes a Spot instance back, your application has 120 seconds. Your code must handle SIGTERM: drain in-flight requests and refuse new ones. Without that, Spot will drop connections.
  • Multi-arch builds. A Python app that depends on an unmaintained C extension may not build on ARM. Test the arm64 image in CI before you cut over.
  • The "oops" deletion. Automated cleanup can delete a volume that was only detached for maintenance. Always snapshot before you delete.
  • Idle shutdown surprises. Stopped RDS instances take several minutes to start. Schedule the morning start well before the first engineer logs in.
  • The savings trap. Spending 40 hours of engineering time to save $50 a month is negative ROI. Work from the top of your bill down.
Free PDF checklist

The 10-point AWS cost checklist

The checks I run first on an AWS bill, each with the AWS CLI command that finds the waste and what to do about it. You can also read it on the site.

No spam. Unsubscribe any time.

The Payoff

Cost optimization is not about being cheap; it is about capital efficiency. Every dollar spent on idle CPU is a dollar you can't spend on hiring or runway. Treat the bill as an architecture problem, fix the seven leaks above, and put a cost check in every pull request so they stay fixed.

Once you know where the money goes, the next step is making it visible to finance. I wrote a guide on syncing AWS billing into Odoo ERP so your CFO gets cost data without bothering engineering.


Not sure which of the seven applies to you? That's what the free review is for: we look at your bill together and you leave with the top 3.

Frequently Asked Questions

How do I cut my AWS compute bill?
Treat it as an architecture problem, not a finance one. The three moves that deliver the biggest compute savings are: run stateless workloads on EC2 Spot Instances (up to 90% off On-Demand), replace the Cluster Autoscaler with Karpenter so you provision the instance types your pods actually need, and migrate compatible services to ARM-based Graviton for better price performance. Then remove NAT Gateway data processing, idle non-prod hours, oversized instances and orphaned storage.
Which workloads are safe to run on AWS Spot Instances?
Stateless microservices, background/batch workers (Sidekiq, Celery), and CI jobs are ideal for Spot because they tolerate interruption. Keep stateful systems like production databases on On-Demand. The practical setup is a cluster that routes stateless pods to Spot nodes and stateful pods to On-Demand nodes automatically.
What is the difference between Karpenter and the Cluster Autoscaler?
The Cluster Autoscaler scales fixed Auto Scaling Groups, so it can only add pre-defined instance types even when your pods need something smaller or different. Karpenter looks at pending pods, calculates the CPU and memory required, and launches a fitting instance type (including mixing Spot and architectures) directly through the EC2 API, which improves bin-packing and cuts waste.
How much can migrating to AWS Graviton save?
AWS states Graviton (ARM) instances cost up to 20% less than comparable x86 instances. Because Go, Python, Node.js, and Java run on ARM, migration is often just producing multi-architecture container images and deploying to Graviton nodes.
Is running production workloads on Spot Instances risky?
Spot is safe for interruption-tolerant workloads if your app handles the 2-minute reclamation gracefully. Your services must catch SIGTERM, drain in-flight requests, and stop accepting new ones before shutdown, and your cluster needs interruption handling (Karpenter's native interruption handling or the AWS Node Termination Handler). Without that, Spot reclamations will drop connections.
Why is my AWS bill so high even when traffic is low?
Almost always over-provisioning and always-on resources. Teams size instances and replica counts for worst-case spikes out of fear of downtime, so average CPU utilization stays low, and dev/staging environments run 24/7 for a 40-hour work week. Rightsizing, autoscaling and scheduled non-prod shutdowns recover that spend.
Why are AWS NAT Gateways so expensive, and how do I reduce the cost?
NAT Gateways charge about $0.045 per GB processed on top of the hourly fee, so private instances or EKS pods pulling images from ECR or reading large volumes from S3 pay a premium on traffic that never leaves AWS. Routing that traffic through VPC Gateway and Interface Endpoints instead of the NAT Gateway removes most of that charge.
How do I catch AWS cost increases before deploying?
Add Infracost to your CI/CD pipeline so every Terraform pull request gets an automated estimate of its cost impact. If a change bumps an RDS instance from db.t3.medium to something far larger, the PR is flagged with the projected monthly increase before merge, instead of showing up on next month's bill.
Is resource tagging enough to control AWS costs?
No. Tagging is necessary for cost allocation but it is reactive and only tells you where money already went. Durable savings come from fixing expensive architecture, automating cleanup of waste, and shifting cost checks into pull requests, not from tags and monthly spreadsheet reviews.

AWS Cost Audit · $250 fixed

AWS bill climbing and nobody knows why?

I go through your AWS bill and account (Cost Explorer or your Cost & Usage Report, VPC setup, compute and storage) and find where the money leaks.

  • Written findings, ranked by estimated monthly savings
  • Terraform patches for the top fixes, ready to review and apply
  • Notes on risk and rollout order for each change
  • Delivered within 48 hours

Not ready for a call? Estimate your waste in a minute with the free AWS cost waste calculator.

Mohamed ARKID
Mohamed ARKID

DevOps & Cloud Consultant

I build systems that run reliably, scale efficiently, and deploy intelligently. See how I can help your team.