AWS

AWS Costs Explained: What Actually Charges You (April 2026)

We just spent a week testing EKS Pod Identity. If we were paying full price instead of using credits, the lab would have cost $394/month in running resources even at 2 a.m. on a Sunday. Here is exactly what AWS charges for, what tutorials never tell you, and how to avoid the bills that make people ragequit AWS.

Original content from computingforgeeks.com - post 165604

Most AWS cost articles recycle the same generic advice: right-size, buy reserved instances, delete unused stuff. None of them show you an actual account. This one does. Every dollar figure below comes from a real lab we ran while writing our EKS Pod Identity guide, cross-checked against the current AWS pricing pages on the day of publication. We cover the NAT Gateway trap, the public IPv4 surcharge that hit everyone in 2024, the EKS Extended Support 6x price jump, Aurora Serverless minimums, and the nine other traps that quietly drain test accounts.

The goal is a single honest reference you can send to anyone who got blindsided by an AWS bill. No filler. No affiliate links. No “top 10 money-saving tips” marketing copy. Just the rates, the math, the CLI commands to find waste in your own account, and a real case study of our own lab getting out of control and how we cleaned it up.

Verified April 2026 with AWS pricing pages pulled fresh during publication. All numbers below are current on-demand prices in us-east-1 unless otherwise noted.

What’s Actually Free on AWS (and What Isn’t)

AWS overhauled the Free Tier on July 15, 2025. New accounts now get a Free Plan: $100 in credits, 6 months, limited service access, then either upgrade to Paid or the account auto-closes. Accounts opened before that date still run on the older 12-month trial model until their 12 months expire.

The always-free offers did not change. These work on any account, forever, with no expiry:

  • Lambda: 1 million requests/month plus 400,000 GB-seconds of compute. Genuinely generous.
  • DynamoDB: 25 GB storage, 25 write capacity units, 25 read capacity units
  • SNS: 1 million publishes, 100,000 HTTP deliveries
  • SQS: 1 million requests
  • CloudWatch: 10 custom metrics, 10 alarms, 1 million API requests, 5 GB of Logs ingestion
  • KMS: 20,000 symmetric request operations
  • Data transfer out: 100 GB/month to the internet, aggregated across all services, since 2021

The 12-month EC2/RDS/S3 trial (750 hours of t2/t3/t4g.micro, 20 GB RDS storage, 5 GB S3 Standard) only applies to accounts opened before July 15, 2025.

Here is the honest part nobody spells out. These services are never free, not even on a brand new account:

  • NAT Gateway: $0.045/hour from the first second it exists (us-east-1). The biggest free-tier surprise by far.
  • Elastic IPs: $0.005/hour whether attached, unattached, or idle, since February 2024
  • Any public IPv4 address on any AWS resource: $0.005/hour = $3.60/month per IP
  • EKS control plane: $0.10/cluster/hour = $73/month per cluster, always
  • KMS customer-managed keys: $1/month per key
  • Secrets Manager: $0.40/secret/month
  • Multi-AZ RDS: Always paid, the free tier only covers Single-AZ
  • VPC Interface Endpoints (PrivateLink): $0.01/endpoint/AZ/hour
  • Data processing above free tier thresholds on NAT Gateway, ALB, NLB

If you provision any of the above and forget about them, your “free” AWS account will quietly accumulate charges. We have seen people get a $60 bill their first month from a single forgotten NAT Gateway.

The gap between “what the marketing page promises” and “what actually bills” is where most of the confusion lives. AWS sells the Free Tier as a way to try services without commitment, and for some workloads that works. Build a Lambda-only API, stay inside the always-free thresholds, and you can run a small SaaS for free for years. Spin up a three-AZ EKS cluster with managed node groups and expect the same outcome, and the $219 bill that arrives on the 1st of next month is going to feel like a betrayal. It is not. It is the logical result of defaults AWS picked for production resilience, applied to a workload that did not need them.

The right mental model is that AWS prices everything assuming you want production-grade availability, and the Free Tier is a narrow slice of services where a single failure is not catastrophic. Anything that involves high availability primitives (Multi-AZ, NAT Gateways, load balancers, managed Kubernetes control planes) falls outside that slice by design.

The Real Numbers from Our Pod Identity Test Week

Here is our actual lab, running in eu-west-1 (Ireland), at the moment we started writing this article. The account has $720 in credits so we paid nothing. You would have paid all of it.

First, the inventory. We ran these commands against the lab account to build a full picture of what was running:

aws eks list-clusters --region eu-west-1 --query 'clusters[]' --output text
aws ec2 describe-instances --region eu-west-1 \
  --filters "Name=instance-state-name,Values=running" \
  --query 'Reservations[].Instances[].[InstanceId,InstanceType,Tags[?Key==`Name`].Value|[0]]' \
  --output text
aws ec2 describe-nat-gateways --region eu-west-1 \
  --filter "Name=state,Values=available" \
  --query 'NatGateways[].[NatGatewayId,VpcId,State]' --output text
aws elbv2 describe-load-balancers --region eu-west-1 \
  --query 'LoadBalancers[].[LoadBalancerName,Type,State.Code]' --output text
aws ec2 describe-volumes --region eu-west-1 \
  --query 'Volumes[].[VolumeId,Size,VolumeType,State]' --output text

What came back was revealing. Two NAT Gateways instead of one, six orphaned EBS volumes left behind by earlier experiments, and two EKS clusters where we only remembered creating one:

pod-identity-lab
pod-identity-lab-2

i-0a1b2c3d4e5f67890  t3.medium  pod-identity-lab worker
i-0fedcba987654321f  t3.medium  pod-identity-lab-2 worker
i-0123456789abcdef0  t3.medium  pod-identity-lab worker

nat-0a1b2c3d4e5f67890  vpc-0a1b2c3d4e5f67890  available
nat-0fedcba987654321f  vpc-0fedcba987654321f  available

k8s-demo-nginx-380f880b9d  application  active

vol-0a1b2c3d4e5f67891    8GB  gp2  available
vol-0a1b2c3d4e5f67892   80GB  gp3  in-use
vol-0a1b2c3d4e5f67893   32GB  gp2  available
vol-0a1b2c3d4e5f67894   10GB  gp2  available
vol-0a1b2c3d4e5f67895    1GB  gp2  available
vol-0a1b2c3d4e5f67896    1GB  gp2  available
vol-0a1b2c3d4e5f67897   80GB  gp3  in-use
vol-0a1b2c3d4e5f67898  128GB  gp2  available
vol-0a1b2c3d4e5f67899   80GB  gp3  in-use

Now the math. At eu-west-1 on-demand pricing, this is what the lab costs to run for 30 days:

ResourceCountUnit rateMonthlyNotes
EKS cluster (standard support)2$0.10/hr$146.00Always-on control plane
EC2 t3.medium (eu-west-1)3$0.0456/hr$99.86Worker nodes
NAT Gateway (eu-west-1)2$0.048/hr$70.08One per VPC, the silent killer
ALB1$0.0252/hr$18.40Plus LCU charges
EBS gp3 attached240 GB$0.088/GB-mo$21.12Worker node root volumes
EBS gp2 orphaned180 GB$0.110/GB-mo$19.80Left behind by prior tests
Public IPv4 on NAT/ALB3$0.005/hr$10.95One per NAT plus one per ALB IP
Total (idle, before data transfer)$386.21/mo

Three hundred eighty-six dollars per month. For a test lab we only actively used during business hours on two days. The cluster sits there at 2 a.m. on Sunday burning $12/day in idle charges, then another $12/day Monday while we sleep, and so on.

Three things compound to create this number. First, NAT Gateway bills whether traffic flows through it or not, so even a quiet cluster pays hourly rent. Second, eksctl creates a new VPC per cluster by default, which means every eksctl create cluster invocation spins up a new NAT Gateway, and we had two clusters in two VPCs. Third, orphaned EBS volumes from destroyed instances never disappear on their own because default EBS behaviour preserves volumes when the instance terminates, unless you explicitly set DeleteOnTermination.

The bill Cost Explorer would have shown you if we had paid:

aws ce get-cost-and-usage \
  --time-period Start=2026-04-01,End=2026-04-08 \
  --granularity DAILY \
  --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=SERVICE \
  --region us-east-1

Against our inventory, the top three lines would have been Amazon EKS ($33 for the week), Amazon EC2 excluding NAT ($27 for compute plus $24 for NAT hours), and Amazon VPC ($26 for public IPv4). Nothing dramatic happened. No spikes. No unusual workloads. Just the baseline of what a real lab costs to keep around.

That is the part that catches first-time EKS users off guard. The tutorial tells you to run eksctl create cluster and it spins up 23 AWS resources behind the scenes: a VPC, six subnets (three public, three private), an internet gateway, route tables, one NAT Gateway (or three if you enable multi-AZ), a managed node group, an IAM role, a KMS key, a CloudWatch log group, and the EKS control plane. Every one of them has a price tag or a pricing dimension attached. You only typed two commands.

The second thing worth calling out is how long the bill persists after you stop working. Our lab was idle for 18 hours a day (we ran tests for about 6 hours, then went to bed, then did other things the next day). The billing did not care. A NAT Gateway costs $0.048/hour whether your cluster is processing 10,000 requests or zero. EKS control planes bill by the hour regardless of pod count. EBS volumes bill by GB-month regardless of IOPS used. The entire architecture is built around “pay for capacity, not utilization,” and that is exactly why idle test labs are expensive.

The 12 Worst AWS Cost Traps

These are the ones that cause real pain. Each comes with the actual pricing math, the mistake that triggers it, and how to detect or prevent it.

1. NAT Gateway, the number one money drain

NAT Gateway costs $0.045/hour plus $0.045/GB processed in us-east-1, and $0.048 plus $0.048/GB in eu-west-1. The hourly rate alone is $32.85/month in us-east-1, $35.04/month in eu-west-1, before a single byte crosses it. Multi-AZ tutorials typically provision one per AZ, so a three-AZ deployment runs $98 to $105/month just to exist.

Almost every EKS tutorial and every Terraform module for a “production-ready VPC” provisions one NAT Gateway per AZ. The rationale is sound: if the AZ hosting your single NAT goes down, your private subnets in the other AZs lose internet access. For a real production workload that matters. For a dev cluster that runs for three days, it just triples the NAT bill. A single-NAT dev VPC is a perfectly reasonable trade-off, and you can add the extra NATs back when you promote the environment.

The worst part is the double charge on internet egress. When traffic leaves a private subnet, hits NAT, and goes to the internet, you pay NAT processing ($0.045/GB) plus internet egress ($0.09/GB) for an effective rate of $0.135/GB. Pull a 50 GB container image from a public registry through NAT and you owe $6.75 every time.

To find every NAT Gateway in an account, use this:

aws ec2 describe-nat-gateways \
  --filter "Name=state,Values=available" \
  --query 'NatGateways[].[NatGatewayId,VpcId,SubnetId,CreateTime]' \
  --output table

The fix: use VPC Gateway Endpoints (free) for S3 and DynamoDB, use VPC Interface Endpoints for the AWS APIs you call most (cheaper than NAT past a few GB/day), collapse dev clusters into a single VPC, and delete unused VPCs with their NAT Gateways aggressively.

There is one more pattern worth knowing. If your workload only calls AWS APIs (Secrets Manager, SSM Parameter Store, ECR pulls, STS, S3, DynamoDB), you do not need a NAT Gateway at all. Put the workload in a private subnet, add Gateway Endpoints for S3 and DynamoDB, and Interface Endpoints for the half-dozen APIs you actually use, and the NAT Gateway becomes optional. For an EKS cluster that only talks to AWS services and internal APIs, this saves the entire $35/month/AZ NAT bill in exchange for roughly $15/month per Interface Endpoint. At three or more NATs the endpoints win. At one NAT the endpoints tie. Do the math for your specific workload.

2. EKS Extended Version Support, the 6x silent upgrade

Standard EKS support costs $0.10/cluster/hour ($73/month). When your Kubernetes version falls out of standard support, AWS automatically moves the cluster to Extended Support at $0.60/cluster/hour ($438/month). That is a 6x overnight jump. A forgotten dev cluster running an outdated version silently becomes a $1,300/quarter liability.

Check for clusters approaching end of standard support:

aws eks list-clusters --query 'clusters[]' --output text | \
  xargs -I {} aws eks describe-cluster --name {} \
  --query 'cluster.[name,version,status]' --output text

Cross-reference the version column against the EKS supported versions calendar, and plan upgrades before the standard window closes. Set a budget alert specifically on the EKS line item so a sudden 6x jump screams immediately.

3. Idle Elastic IPs

Since February 2024, every Elastic IP bills $0.005/hour whether attached to a running instance, attached to a stopped instance, or fully unallocated. $3.60/month per IP. Stop an EC2 instance overnight and the EIP still charges you.

Find all idle EIPs in the current region:

aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId,Domain]' \
  --output table

Release them with aws ec2 release-address --allocation-id eipalloc-xxx. If you need a stable IP only occasionally, allocate it when needed and release it afterwards, or switch to a DNS name in front of a dynamic IP.

4. The public IPv4 surcharge (the 2024 change nobody noticed)

On February 1, 2024, AWS started charging $0.005/hour for every public IPv4 address on every resource: EC2, NAT Gateway, ALB, NLB, RDS, ElastiCache, EKS nodes with public IPs. $3.60/month per IP, attached or idle. An Auto Scaling group of 10 instances with public IPs plus an ALB plus a NAT Gateway racks up 12+ public IPs, or $43/month in pure IP rent. AWS posted the change here.

The surcharge does not apply to IPv6 addresses or BYOIP. The cheapest fix is to put your workloads in private subnets behind a single NAT or an ALB, so only the load balancer pays for a public IP.

Public IP Insights (introduced alongside the surcharge) is a free tool in the AWS console that shows every public IPv4 address in your account, which service it is attached to, and the hourly rate. Turn it on, scan the account quarterly, and release anything that does not need to be public. Most EC2 instances in a well-designed VPC have no reason to hold a public IP since the load balancer handles public-facing traffic on their behalf.

5. Cross-AZ data transfer

Data between AZs in the same region costs $0.01/GB each way. That is $0.02/GB round-trip. It bites every Multi-AZ database replicating to its standby, every Kubernetes pod-to-pod call that lands on the wrong AZ, and every ALB that balances traffic across AZs (which is the default).

A chatty microservice fleet exchanging 5 TB/month across AZs costs $50/month in transfer. Multiply across services and the bill turns into real money. Fix: use topology spread constraints or topology-aware routing in Kubernetes, keep chatty services co-located, use single-AZ dev environments, and profile Multi-AZ replication traffic on RDS.

The nasty part of cross-AZ is that it is hidden inside services you already pay for. RDS Multi-AZ replicates synchronously from primary to standby across AZs. Aurora replicates the redo log to multiple AZs. EKS worker nodes in different AZs need to talk to each other for inter-pod traffic, for coredns queries that hit a pod in the “wrong” AZ, and for load-balancer health checks that pass through the NLB fabric. You never see any of that in the console as a line item until you pull Cost Explorer and group by USAGE_TYPE, and then it shows up as EU-DataTransfer-Regional-Bytes quietly racking up hundreds of dollars.

6. Data transfer out to the internet

Internet egress in us-east-1: first 100 GB/month free, next 9,999 GB at $0.09/GB, next 40 TB at $0.085/GB, next 100 TB at $0.07/GB, over 150 TB at $0.05/GB. Serving 1 TB/day from S3 directly costs $90/day, or $2,700/month. Putting CloudFront in front drops the rate and adds caching, often halving the bill at any meaningful scale.

For anyone serving video, large images, software downloads, or API responses at volume, data transfer out will eventually be the biggest single line on the bill. It creeps up silently because no single request feels expensive. $0.09/GB sounds trivial until you realize that a 10 MB page served to 1 million users is 100 TB, which is about $9,000. CloudFront caching sharply reduces origin egress because cached responses ship from the CloudFront edge at lower rates, and the first 1 TB/month from CloudFront to the internet is free on top of that.

As of March 2025, AWS waives data transfer out charges for customers leaving AWS if you contact Support and request it. You do not need to close the account.

7. Forgotten EBS snapshots

Snapshots cost $0.05/GB-month of changed blocks (Standard tier) or $0.0125/GB-month (Archive tier, 90-day minimum). A nightly snapshot job on a 500 GB volume with no lifecycle policy quietly grows for months. After a year, a “routine” backup schedule for one volume can exceed 2 TB of retained delta blocks, which is $100/month for data you will never restore.

Audit with:

aws ec2 describe-snapshots --owner-ids self \
  --query 'Snapshots[?StartTime<=`2025-10-01`].[SnapshotId,VolumeSize,StartTime,Description]' \
  --output table

Every EBS-backed workload needs a Data Lifecycle Manager policy that expires snapshots after a defined window (7, 14, 30 days depending on your RPO).

8. CloudWatch Logs with no retention policy

CloudWatch Logs Standard ingestion is $0.50/GB for the first 10 TB in us-east-1. Storage is $0.03/GB-month. Default log group retention is never expire. A chatty application logging 10 GB/day costs $150/month in ingestion alone, and the storage bucket grows forever on top.

The Infrequent Access class (launched 2024) is half the ingestion rate at $0.25/GB and, as of March 2026, supports Logs Insights queries, which makes it viable for the vast majority of application logs. Set retention on every log group explicitly and switch chatty groups to IA:

aws logs put-retention-policy \
  --log-group-name /aws/lambda/my-function \
  --retention-in-days 14

Audit every log group across the account for retentionInDays: None once a quarter and fix anything that comes back.

9. Fargate minimum billing

Fargate Linux x86 bills $0.04048/vCPU-hour and $0.004446/GB-hour, with a one-minute minimum per task (five minutes for Windows). A workflow that spawns 1,000 very short tasks a day pays for 1,000 full minutes even if the actual work takes 5 seconds each. At 0.25 vCPU and 1 GB, that is roughly $1.50/day for 5,000 seconds of real work. Batch up short tasks when you can, or run them on a persistent EC2 worker.

10. Multi-AZ on toy dev environments

RDS Multi-AZ doubles both compute and storage. Aurora Multi-AZ adds one reader per additional AZ. EKS with worker nodes across three AZs needs at least three of every DaemonSet pod and pays cross-AZ for everything. None of that matters in a dev environment where an hour of downtime costs nothing.

Run dev clusters in a single AZ, run dev databases Single-AZ, and skip the standby entirely when the environment is disposable. Reserve Multi-AZ for staging and production where the extra resilience is worth paying for.

11. Aurora Serverless v2 minimum ACU

Aurora Serverless v2 bills $0.12/ACU-hour. The minimum is 0.5 ACU, which is $43.80/month per cluster even when idle. That was the deal for years: v2 could not scale to zero like v1 could. AWS added scale-to-zero to Aurora Serverless v2 for some engine versions in late 2024, but it takes effect only after a cooldown period and it is not the default. Verify your cluster actually drops to 0 ACU during idle periods:

aws rds describe-db-clusters \
  --db-cluster-identifier my-cluster \
  --query 'DBClusters[0].ServerlessV2ScalingConfiguration'

If MinCapacity is 0.5, you are paying the minimum. Change it to 0 if your engine version supports it and your workload tolerates a cold-start delay.

12. Interface VPC endpoints stacked everywhere

Interface Endpoints bill $0.01 per endpoint per AZ per hour plus $0.01/GB processed. Two AZs running 24/7 per endpoint = $14.60/month before a single byte. A locked-down VPC with 10 Interface Endpoints across 3 AZs = $219/month just for the endpoints.

S3 and DynamoDB have free Gateway Endpoints. Use those. Reserve Interface Endpoints for services that genuinely need them (Secrets Manager, ECR, SSM) and consolidate to fewer AZs in dev.

The confusing part is that the AWS docs push Interface Endpoints hard for "best practice" private connectivity, without ever mentioning that Gateway Endpoints exist for the two services that handle the vast majority of traffic. Always check whether a Gateway Endpoint is available first. Second, always size the endpoint count against the traffic savings. If your workload only pulls a few hundred MB/month through NAT, an Interface Endpoint does not pay for itself. If it pulls tens of GB, it does, and quickly.

The 2024 to 2026 Pricing Changes You Might Have Missed

AWS pricing shifts yearly, and these four changes quietly rewrote what a small account actually costs to run. If you last built mental models in 2022, they are out of date.

  • Feb 2024, public IPv4 surcharge: $0.005/hour per public IPv4 address on every service. The single biggest base-rate change in years.
  • 2024, CloudWatch Logs Infrequent Access ingestion: Half the price of Standard at $0.25/GB. As of March 2026 it supports full Logs Insights, making it the default choice for application logs.
  • March 2025, free data transfer out when leaving AWS: Contact Support to waive DTO charges when migrating off. Does not require closing the account.
  • July 2025, Free Tier overhaul: 12-month trial replaced with a 6-month Free Plan ($100 credit), then upgrade or the account auto-closes. Always-free offers were unchanged.
  • Late 2025, Database Savings Plans: Up to 35% off Aurora, RDS, DynamoDB, ElastiCache, Neptune, DocumentDB with a one-year commit, flexible across engines.
  • EKS Extended Support: $0.60/hour kicks in automatically when your Kubernetes version falls out of standard support. Plan upgrades proactively.

How to Find What's Costing You (Investigation Commands)

Before you start cutting, find out where the money actually goes. These are the commands we run first on any account we inherit. All of them read-only, safe to run, and the output is what drives every subsequent decision.

The top-level question is "which services are charging me?" Cost Explorer answers it grouped by SERVICE:

aws ce get-cost-and-usage \
  --time-period Start=2026-03-01,End=2026-04-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=SERVICE \
  --region us-east-1

The output lands as JSON with one group per service, sorted alphabetically. Pipe it through jq to sort by cost descending and you get an instant ranking of where the money lives.

The service-level view hides the interesting detail. EC2-Other in particular is a grab-bag that includes NAT hours, NAT processing, EBS, and transfer charges. Drill into USAGE_TYPE to separate them:

aws ce get-cost-and-usage \
  --time-period Start=2026-03-01,End=2026-04-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=USAGE_TYPE \
  --filter '{"Dimensions":{"Key":"REGION","Values":["eu-west-1"]}}' \
  --region us-east-1

Against our lab, the Cost Explorer output separates NatGateway-Hours, NatGateway-Bytes, EU-DataTransfer-Out-Bytes, and EU-BoxUsage:t3.medium as distinct line items. Once you see which USAGE_TYPE is fattest, the fix is usually obvious.

Next, find forgotten compute. Every running EC2 instance you were not expecting is a bill waiting to happen:

aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" \
  --query 'Reservations[].Instances[].[InstanceId,InstanceType,LaunchTime,Tags[?Key==`Name`].Value|[0]]' \
  --output table

Anything with a launch time older than the last time you touched that project needs a second look.

Orphaned EBS volumes are the ones that got us. Volumes in the available state belong to no instance and bill forever:

aws ec2 describe-volumes \
  --filters "Name=status,Values=available" \
  --query 'Volumes[].[VolumeId,Size,VolumeType,CreateTime]' \
  --output table

When we ran this against the lab, six orphaned volumes totalling 180 GB came back. Every one of them was billing.

Check for unassociated Elastic IPs and idle NAT Gateways the same way:

aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' --output table
aws ec2 describe-nat-gateways \
  --filter "Name=state,Values=available" \
  --query 'NatGateways[].[NatGatewayId,VpcId,CreateTime]' --output table

Snapshots accumulate the most quietly because nobody audits them until the bill is already fat:

aws ec2 describe-snapshots --owner-ids self \
  --query 'Snapshots[].[SnapshotId,VolumeSize,StartTime]' \
  --output text | sort -k3

Beyond the CLI, three AWS-native tools deserve a slot in your toolbox. Trusted Advisor (free tier gets a handful of cost-optimization checks for idle load balancers, unassociated EIPs, and low-utilization EC2 when you have Business or Enterprise support). Compute Optimizer (free, gives right-sizing recommendations for EC2, EBS, Lambda, and Fargate across the whole account). Athena on Cost and Usage Reports is the nuclear option: enable CUR to an S3 bucket, crawl with Glue, and write SQL against every single line item. When you need to answer questions like "which Kubernetes namespace drove the most NAT traffic last month" this is the only tool that can do it.

The order we run them is always: Cost Explorer first (for the shape of the bill), then CLI describe calls (to find specific offending resources), then Compute Optimizer (for right-sizing), then Athena on CUR only when the first three cannot answer the question. Most cost investigations stop at step two because the culprit is almost always obvious once you group by USAGE_TYPE. The NAT line will be bigger than you expected. The EKS line will be bigger than you remember spinning up. Start there.

One extra trick: CloudWatch publishes AWS/NATGateway BytesOutToDestination and BytesInFromDestination metrics per NAT Gateway. If you have three NATs and one of them is suspiciously expensive, you can tell which one is the problem without enabling VPC Flow Logs. Graph both metrics in CloudWatch, sort by NAT Gateway ID, and the offender is whichever one has the tall bars. From there, trace which subnets route to it and which workloads live in those subnets.

How to Prevent Surprises

Investigation finds waste that already exists. Prevention stops it from accumulating in the first place. Six practices handle 90% of AWS cost surprises.

Enable Cost Anomaly Detection today. It is free. AWS-side ML watches for unusual spend patterns by service or by tag and alerts you when something jumps. The whole setup takes two minutes in the console:

aws ce create-anomaly-monitor \
  --anomaly-monitor '{"MonitorName":"daily-service-monitor","MonitorType":"DIMENSIONAL","MonitorDimension":"SERVICE"}'

Wire the resulting monitor to an SNS topic that posts to Slack, or accept the daily email digest. Either way, a surprise $500 spike stops being a month-end discovery.

AWS Budgets with actions, not just alerts. A budget alert tells you that you blew through $100. A budget action actually stops the bleeding by attaching an IAM policy or an SSM document. Set a monthly budget per account, with warnings at 50%, 80%, and 100%, and an action at 110% that applies a deny policy to expensive services like EC2 RunInstances in the dev account.

Mandatory tagging at creation. Four tags cover most use cases: environment, owner, project, cost-center. Enable them as cost allocation tags in the billing console (takes up to 24 hours to propagate), then enforce them with Tag Policies in Organizations. Any resource without all four is untracked spending.

Service Control Policies in Organizations. In dev accounts, deny the things you never want accidentally created: ec2:CreateNatGateway, rds:CreateDBInstance with any db.r* family, regions outside your footprint. An SCP is the only AWS primitive that blocks both humans and roles, including the account root user.

Auto-shutdown schedules for dev resources. Lambda plus EventBridge plus a tag like AutoShutdown=true stops tagged instances at 19:00 weekdays and all weekend. Even a naive implementation saves roughly 70% on compute for anything that doesn't need to run 24/7. The pattern:

  • EventBridge schedule at cron(0 19 ? * MON-FRI *) triggers a "stop" Lambda
  • EventBridge schedule at cron(0 8 ? * MON-FRI *) triggers a "start" Lambda
  • Both Lambdas query EC2 (and RDS, if needed) for instances with AutoShutdown=true and act on them
  • A DynamoDB table records state transitions for audit

Cleanup automation. A weekly Lambda that deletes snapshots older than 30 days (unless tagged keep=true), releases EIPs with no association, and flags EBS volumes in available status for manual review catches the long tail. Write it once, run it forever.

One more subtle one: deny-by-default regions. Most accounts only operate in two or three AWS regions. An SCP that denies every action outside those regions blocks the entire class of "someone launched a t2.micro in ap-southeast-3 and forgot it existed" bugs. It also blocks the less common but more painful version where an attacker with compromised credentials spins up crypto-mining instances in a region you never monitor. The SCP is four lines of JSON and it costs nothing:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "NotAction": ["iam:*","organizations:*","route53:*","cloudfront:*","support:*","s3:GetAccountPublicAccessBlock"],
    "Resource": "*",
    "Condition": {
      "StringNotEquals": {
        "aws:RequestedRegion": ["us-east-1","eu-west-1"]
      }
    }
  }]
}

Attach the SCP at the Organizations OU level and every account inside the OU gets the same restriction automatically.

Quick Reference Price Table

Bookmark this section. Every rate below is current on-demand pricing in us-east-1 as of April 2026, pulled from the official pricing pages. Regions other than us-east-1 run 5 to 15% higher on most services.

ServiceRateMonthly (730 hrs)Notes
NAT Gateway$0.045/hr + $0.045/GB$32.85 + dataus-east-1
NAT Gateway (eu-west-1)$0.048/hr + $0.048/GB$35.04 + dataAll EU regions slightly higher
EKS control plane (standard)$0.10/hr$73.00Per cluster
EKS Extended Support$0.60/hr$438.00Auto-applied when K8s out of standard support
ALB$0.0225/hr + $0.008/LCU-hr$16.43 + LCU1 LCU max of 25 conn/s, 3000 active, 1 GB/hr, 1000 rules/s
NLB$0.0225/hr + $0.006/NLCU-hr$16.43 + NLCUTLS termination expensive (50 conn/s per NLCU)
t3.micro Linux$0.0104/hr$7.59On-demand
t3.medium Linux$0.0416/hr$30.37On-demand
EBS gp3$0.08/GB-monthvaries3000 IOPS + 125 MB/s free, extra IOPS $0.005/mo
EBS gp2 (legacy)$0.10/GB-monthvariesSwitch to gp3, it's cheaper
EBS snapshots Standard$0.05/GB-monthvariesChanged blocks only
EBS snapshots Archive$0.0125/GB-monthvaries90-day minimum, $0.03/GB retrieval
S3 Standard (first 50 TB)$0.023/GB-monthvaries$0.022 next 450 TB, $0.021 above
S3 Standard-IA$0.0125/GB-monthvariesPlus retrieval fees
Data transfer out (first 10 TB)$0.09/GBvariesFirst 100 GB/month free globally
Cross-AZ data transfer$0.01/GB each wayvaries$0.02 round-trip
Fargate Linux x86$0.04048/vCPU-hr + $0.004446/GB-hrvaries1-minute minimum per task
Fargate Linux ARM$0.03238/vCPU-hr + $0.003560/GB-hrvaries~20% cheaper than x86
Public IPv4 address$0.005/hr$3.60Per IP, attached or idle, since Feb 2024
Elastic IP (unattached)$0.005/hr$3.60Same rate as attached
KMS customer-managed key$1/month$1.00+ $0.03/10k symmetric requests
Secrets Manager$0.40/secret/mo$0.40/secret+ $0.05/10k API calls
CloudWatch Logs Standard$0.50/GB ingest + $0.03/GB storedvariesFirst 10 TB
CloudWatch Logs IA$0.25/GB ingest + $0.03/GB storedvariesLaunched 2024, full Insights support Mar 2026
CloudWatch custom metrics$0.30/metric/mo$0.30 eachFirst 10 free
VPC Interface Endpoint$0.01/endpoint/AZ/hr + $0.01/GB$14.60 (2 AZs)Per endpoint
Aurora Serverless v2$0.12/ACU-hr$43.80 min0.5 ACU minimum unless scale-to-zero enabled
RDS db.t3.micro Single-AZ$0.017/hr$12.41MySQL/PostgreSQL
RDS db.t3.micro Multi-AZ$0.034/hr$24.82Compute only, storage doubles too

The authoritative pages for any number above: Amazon VPC pricing, Amazon EKS pricing, and EC2 on-demand pricing.

Mini Case Study: Cleaning Up Our Lab

Here is what we did to the Pod Identity lab after the article shipped. Total charges avoided: $386.21/month.

Step one, delete both EKS clusters. eksctl is the easiest way because it also tears down the VPC, subnets, route tables, NAT Gateways, IAM roles, and node groups it created:

eksctl delete cluster --name pod-identity-lab --region eu-west-1
eksctl delete cluster --name pod-identity-lab-2 --region eu-west-1

Both deletions take 15 to 20 minutes because CloudFormation has to roll back every resource in the cluster stack in the right order. Watch the CloudFormation console to confirm the stacks reach DELETE_COMPLETE, because a failed rollback leaves paying resources behind.

Step two, delete the orphaned EBS volumes. These were not part of any cluster so eksctl did not touch them:

for vol in vol-0408b49226bd47a7c vol-08f82ceebafa6421f vol-0dccaf4acaf508e36 \
           vol-0d065e512eb375945 vol-0045b25d06719a8b1 vol-034d43084d5bc9f2a; do
  aws ec2 delete-volume --volume-id $vol --region eu-west-1
done

Step three, sweep for any stragglers. After CloudFormation finishes, re-run the inventory commands to make sure nothing is left:

aws eks list-clusters --region eu-west-1
aws ec2 describe-instances --region eu-west-1 \
  --filters "Name=instance-state-name,Values=running" \
  --query 'Reservations[].Instances[].InstanceId' --output text
aws ec2 describe-nat-gateways --region eu-west-1 \
  --filter "Name=state,Values=available" --query 'NatGateways[].NatGatewayId' --output text
aws ec2 describe-volumes --region eu-west-1 \
  --filters "Name=status,Values=available" --query 'Volumes[].VolumeId' --output text
aws ec2 describe-addresses --region eu-west-1 \
  --query 'Addresses[].PublicIp' --output text
aws elbv2 describe-load-balancers --region eu-west-1 \
  --query 'LoadBalancers[].LoadBalancerArn' --output text

Every one of those should return empty. If anything remains, there is probably a CloudFormation stack that partially failed. Check the console, fix it, re-run the delete. The lab is clean when every command above prints nothing.

Last step, verify in Cost Explorer tomorrow. Costs after a cleanup take 24 hours to drop to zero because Cost Explorer updates on a delay. If the daily line doesn't collapse the next day, something is still running.

The total time for cleanup was about 30 minutes of wall clock and maybe 90 seconds of actual typing. Most of the wait is CloudFormation grinding through resource deletion in the background. We kept a second terminal open running watch -n 30 'aws cloudformation list-stacks --stack-status-filter DELETE_IN_PROGRESS' and the lab self-erased while we wrote the rest of this article. Every time we looked up, there were fewer stacks. By the time the draft was finished, the inventory commands returned empty and the bill (for a paying account) would have flatlined.

Frequently Asked Questions

Why is my AWS bill so high even though I'm not using much?

The most common cause is always-on infrastructure you forgot about. NAT Gateways ($33/month each), EKS control planes ($73/month each), idle Elastic IPs ($3.60/month each), orphaned EBS volumes ($0.08/GB-month), and CloudWatch Logs with no retention silently accumulate. Run the investigation commands from the section above to find them. Nine times out of ten, a surprise bill is one or two of these items, not a code issue or a traffic spike.

What is a NAT Gateway and why does it cost so much?

A NAT Gateway lets resources in private subnets reach the internet (to pull packages, call APIs, download container images) without having a public IP of their own. It costs $0.045/hour plus $0.045/GB processed in us-east-1, so $32.85/month minimum before any data flows. It is expensive because it is a managed service with built-in high availability per AZ, and because AWS charges both the NAT processing fee and the internet egress fee on traffic leaving to the internet, effectively $0.135/GB. VPC Gateway Endpoints for S3 and DynamoDB bypass NAT entirely and cost nothing.

Are Elastic IPs free on AWS?

No. Since February 2024, every Elastic IP bills $0.005/hour regardless of state, which is $3.60/month per IP. That applies whether the EIP is attached to a running instance, a stopped instance, or unallocated. Before February 2024, AWS only charged for idle EIPs, so older guides still say "EIPs are free when attached." That advice is outdated and will show up on your bill.

How much does EKS cost per month?

The EKS control plane is $0.10/cluster/hour on standard Kubernetes version support, which is $73/month per cluster. When your Kubernetes version falls out of standard support, EKS Extended Support takes over at $0.60/cluster/hour ($438/month), a 6x increase. On top of the control plane you pay for worker nodes (EC2 or Fargate), load balancers, NAT Gateways, EBS volumes, and data transfer. A realistic minimum for a small production EKS cluster with one NAT and a small ALB is $150 to $200/month.

How do I see which resources are costing me the most?

Cost Explorer is the starting point. Group by SERVICE for the high-level view, then drill into USAGE_TYPE to separate NAT hours from NAT processing, EC2 compute from EBS from transfer. For per-resource detail, enable AWS Cost and Usage Reports (CUR) to an S3 bucket and query with Athena. At the CLI layer, aws ce get-cost-and-usage gives you both the service view and the usage-type view, and aws ce get-cost-and-usage-with-resources gives per-resource breakdown for EC2. Compute Optimizer (free) provides right-sizing recommendations for EC2, EBS, Lambda, and Fargate.

Is the AWS Free Tier actually free?

Partially. The always-free offers (Lambda, DynamoDB, SNS, SQS, limited CloudWatch, KMS symmetric requests) are genuinely free on any account, forever. The 12-month trial for EC2 t3.micro, RDS db.t3.micro, and S3 only applies to accounts opened before July 15, 2025, and it is single-AZ only. Accounts opened after that date get the new Free Plan ($100 credit, 6 months). No account has ever included free NAT Gateway, free public IPv4, free EKS control plane, or free Multi-AZ RDS, which is why people keep getting "free tier" bills.

What's the cheapest way to run a small EKS cluster?

Accept that the control plane ($73/month) is non-negotiable. Then minimize everything else: a single AZ for non-production (no cross-AZ charges), one small NAT Gateway or none at all (use VPC Endpoints for S3, DynamoDB, ECR, and STS), two t3.small or t3.medium workers on a Spot Auto Scaling group (roughly 70% off on-demand), gp3 root volumes, a single ALB shared across services with Ingress path routing, and CloudWatch Logs on the IA class with 7-day retention. A lab built that way runs roughly $90 to $120/month. Everything above that is features you are choosing to pay for.

The Cost Discipline Rules

Seven rules every AWS engineer should internalize. Follow all of them and surprise bills mostly stop happening.

  1. Tag everything at creation. Four tags: environment, owner, project, cost-center. Enforce with Tag Policies. Anything without tags is spending you cannot attribute.
  2. Enable Cost Anomaly Detection on day one. It is free, it works, and it catches spikes the same day instead of at month-end close.
  3. Clean up test resources immediately, not later. "Later" means forgotten. The hour you finish testing is the hour you destroy the environment.
  4. Avoid NAT Gateway when you can. VPC Endpoints for AWS APIs cost less past a few GB/day. Consolidate dev VPCs instead of letting every project spawn its own NAT.
  5. Watch Cost Explorer weekly, not monthly. A problem caught on Wednesday costs a third of a problem caught on the 30th of the month.
  6. Assume EKS, Aurora, ElastiCache, and RDS are always-on. Only run what you actually need. There is no "pause" button on most managed data services.
  7. When you stop using an account, delete everything, not just the obvious stuff. Orphaned EBS volumes, snapshots, unallocated EIPs, CloudWatch Logs groups, and KMS keys keep billing long after the instances are gone.

The honest summary is that AWS is not expensive by accident. The defaults favour availability and uptime, which means everything is Multi-AZ, everything has a public IP, every dev cluster gets its own VPC, nothing auto-expires. Those defaults add up. Understanding which of them are actually paying for themselves in your workload (and which ones are just running up the bill while you sleep) is the entire job of cost discipline.

If one thing changes after you read this article, make it Cost Anomaly Detection. Enabling it is two clicks and it catches bills before they hurt. Everything else on the list is harder: restructuring VPCs, cleaning up old snapshots, negotiating Savings Plans, right-sizing instances. Those are multi-day projects. Anomaly Detection is a two-minute task with outsized payoff. Do it first, then come back for the rest when you have time.

For related operational guides, see our kubectl cheat sheet and the Prometheus and Grafana on Kubernetes walkthrough, both of which show setups you can run cheaply on the EKS pattern described above.

Related Articles

Automation Manage VM Instances on Hetzner Cloud using hcloud CLI Cloud AWS Cloud Storage Options – S3, EBS, EFS, FSx Explained Cloud Deploy Collabora Online Office on Ubuntu with Let’s Encrypt SSL Cloud Create CentOS|Ubuntu|Debian VM Templates on OpenNebula

Leave a Comment

Press ESC to close