AWS Cost

AWS Cost Control 101: Cost Explorer, Budgets, Alerts & Free-Tier Guardrails

Quick take: AWS charges you per-second for things you forgot you turned on. The four controls that stop a surprise bill are all free or nearly free: a free-tier usage alert, a Budget with an 80%/100% threshold wired to SNS, a CloudWatch billing alarm (only in us-east-1), and Cost Anomaly Detection. Cost Explorer tells you where the money went; the Cost and Usage Report (CUR) in S3 tells you down to the resource. Set all four on day one, before you launch anything.

A developer spins up a quick demo: a t3.micro behind an Application Load Balancer, an RDS database, and — because a tutorial said so — a NAT gateway in each of three Availability Zones so the private subnets can reach the internet. The EC2 instance is inside the Free Tier, so they assume the whole thing is free. Three weeks later a $180 bill arrives. The instance was indeed free. The three NAT gateways were not: roughly $0.045 per hour each plus $0.045 per GB processed, running 24×7, is about $97/month before a single byte of app traffic — and the ALB, the RDS storage, and a handful of unattached Elastic IPs did the rest. Nobody set an alert, so nobody knew until the invoice landed.

This article is the guardrail that would have caught it on day two instead of day thirty. It is written for someone new to AWS billing but it does not hand-wave: you will learn exactly what the Free Tier covers (and its three very different flavours), how to turn on free-tier usage alerts, how to enable and drive Cost Explorer (filters, group-by, granularity, forecast, RI/Savings Plans reports), how to build every kind of AWS Budget and wire its thresholds to SNS/email, the crucial difference between actual and forecasted alerts, how to add the CloudWatch billing alarm that only lives in us-east-1, how Cost Anomaly Detection learns your baseline, how consolidated billing works across an organization, and how the CUR lands hourly line items in S3 for deep analysis. Every step comes with the Console path, the aws CLI, and Terraform.

By the end you will never be surprised by an AWS bill again — not because AWS got cheaper, but because you built the trip-wires. You will also know the usual suspects behind a shock invoice (NAT gateway hours and data, inter-AZ and internet egress, idle Elastic IPs and EBS volumes, forgotten load balancers and RDS instances, verbose CloudWatch Logs) and exactly which Cost Explorer filter surfaces each one. Cost control on AWS is not an accounting chore you do at month-end; it is four small, mostly free objects you create before you build.

What problem this solves

AWS is pay-as-you-go with no spending cap by default. There is no “stop at $50” switch on a personal account — the platform will happily let a misconfigured resource, a runaway Lambda loop, a leaked access key running crypto-mining, or three forgotten NAT gateways bill you for the entire month. The bill is only tallied and shown to you after the usage happens, and full invoice detail can lag by up to a day. If you have not set up any alerting, the first time you learn about a problem is the invoice — by which point the money is already spent.

What breaks without cost control is not the workload; it is your wallet and your trust in the platform. The three shapes this pain takes are:

Everyone hits at least one of these. Beginners hit all three in their first month. The controls in this article are the standard, AWS-native answer — and, importantly for a learner, they are free or cost cents, so there is no excuse to skip them.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You need an AWS account you can log into as the root user or an IAM/IAM Identity Center principal with billing access, and the AWS CLI v2 configured (aws configure or aws sso login). Terraform is optional but every step includes an aws provider snippet. A few tasks — enabling Cost Explorer, toggling billing preferences, viewing the Bills page — are management-account / root actions in an organization or require an explicit billing IAM policy (aws-portal:*, or the newer fine-grained billing, ce, budgets, cur actions). Nothing here needs you to launch a paid resource.

Where it fits: this is the foundation of the FinOps and governance track, and the first thing you should do in any new account. It sits directly beneath tagging (you tag resources so Cost Explorer and the CUR can slice by team/project — see the companion piece on tagging strategy and cost-allocation enforcement), quotas (service limits stop runaway counts; budgets stop runaway spend — see the companion on service quotas and limit-increase requests), and anomaly troubleshooting (when a spike fires, you triage it — see the companion on cost-spike anomaly-detection troubleshooting). It builds on account structure from AWS Organizations and IAM Foundations and pairs with the network cost knowledge in Amazon VPC, Subnets and Security Groups Explained.

Core concepts

Cost management on AWS is a small vocabulary. Learn these words precisely — the tools, the exam, and the invoices all use them exactly.

Term What it is Where it lives Cost
Billing & Cost Management The console home for all cost tooling Global console (data mostly us-east-1) Free
Free Tier Time-limited or always-free service allowances Billing → Free tier N/A
Cost Explorer (CE) Interactive UI + API to visualise past/forecast cost Billing → Cost Explorer UI free; API $0.01/request
AWS Budgets Threshold alerts on cost/usage/RI/SP + actions Billing → Budgets First 2 free, then $0.02/budget/day
CloudWatch billing alarm A CloudWatch alarm on EstimatedCharges CloudWatch, us-east-1 only Alarm free tier: 10 alarms
Cost Anomaly Detection (CAD) ML that learns your baseline and flags deviations Billing → Cost Anomaly Detection Free
Cost and Usage Report (CUR) Hourly line-item export to your S3 bucket Billing → Data Exports / CUR S3 storage only
Consolidated billing One payer account bills many member accounts AWS Organizations Free
Cost allocation tags Tag keys activated for cost breakdown Billing → Cost allocation tags Free
SNS Pub/sub topic that fans an alert to email/Slack/Lambda Simple Notification Service 1M publishes/mo free
EstimatedCharges The running month-to-date cost metric AWS/Billing namespace, us-east-1 Free
Blended / unblended rate Averaged vs actual per-account rate in an org CE / CUR “cost type” N/A
Amortized cost Upfront RI/SP fees spread across the term CE / CUR “cost type” N/A
Forecast CE/Budgets projection of future spend from history CE / Budgets Free

The single most important mental model is latency. Cost data is not real-time; each tool sees it on a different delay, which is the source of half the “why is it showing nothing?” confusion.

Signal Typical latency Granularity Notes
CloudWatch EstimatedCharges ~6 hours Per-service + total, us-east-1 Cheapest near-real-time signal
Cost Explorer (daily) Up to ~24 hours Daily / monthly Refreshes up to ~3×/day
Cost Explorer (hourly) Up to ~24 hours Hourly (paid opt-in) Enable “hourly & resource-level data”
AWS Budgets evaluation Up to ~8–12 hours Depends on budget Evaluated a few times per day
Cost Anomaly Detection Detects within ~24h of anomaly Per monitor Runs ~3×/day
Cost and Usage Report Up to ~24 hours, refreshed 1–3×/day Hourly line items The most detailed, most delayed
Bills page (invoice) Finalised after month close Per-service, per-region The legal record

Because everything lags, you cannot use these tools to stop spend in real time — you use them to catch a problem within hours, not seconds. The only thing close to a hard stop is a budget action that applies a restrictive policy, and even that fires on the (delayed) budget evaluation, not instantly.

The AWS Free Tier: 12-month, always-free, and trials

“Free Tier” is three completely different things wearing one name. Confusing them is the number-one cause of a beginner’s first surprise bill.

Flavour Duration Resets Example The trap
12-month free 12 months from sign-up Monthly, within the 12 months 750 hrs t2/t3.micro EC2, 5 GB S3 Expires silently at month 13 → full price
Always free Forever Monthly 1M Lambda requests, 25 GB DynamoDB Exceeding the monthly cap bills the overage
Short-term trials Fixed days from first use Does not reset 30-day GuardDuty, 2-month Redshift Starts when you first use it, not at sign-up

The 12-month allowances (the ones that expire)

These are free for 12 months from the day you create the account, then bill at standard rates with no warning other than the alert you set. The headline members:

Service 12-month monthly allowance Then you pay
EC2 750 hrs t2.micro/t3.micro (region-dependent) Linux and 750 hrs Windows ~$0.0116/hr (t3.micro)
EBS 30 GB gp2/gp3, 2M I/Os, 1 GB snapshots ~$0.08/GB-mo (gp3)
RDS 750 hrs db.t2/t3/t4g.micro, 20 GB storage, 20 GB backup Instance + storage rates
ELB 750 hrs Classic or Application LB, 15 GB processed ~$0.0225/hr + LCU
S3 5 GB Standard, 20k GET, 2k PUT ~$0.023/GB-mo
CloudFront (Now always-free) 1 TB out, 10M requests ~$0.085/GB after
RDS/ElastiCache 750 hrs cache.t2/t3.micro (ElastiCache) Node rates
Data transfer (Now always-free) 100 GB/mo out aggregate ~$0.09/GB

The always-free allowances (forever, monthly)

These never expire; you only pay for usage above the monthly cap. The important ones for a beginner:

Service Always-free monthly allowance
Lambda 1M requests + 400,000 GB-seconds compute
DynamoDB 25 GB storage + 25 WCU + 25 RCU (provisioned)
CloudWatch 10 custom metrics, 10 alarms, 1M API requests, 5 GB Logs
SNS 1M publishes, 1k email, 100 SMS (varies)
SQS 1M requests
CloudFront 1 TB data out, 10M HTTP/S requests, 2M CloudFront Functions
Data transfer out 100 GB/mo aggregated across most services
KMS 20,000 requests/mo (keys still ~$1/key/mo)
Cognito 50,000 MAUs (user pools)
Step Functions 4,000 state transitions/mo
X-Ray 100,000 traces recorded/mo
CodeBuild 100 build minutes/mo (general1.small)

Short-term trials (start on first use)

These are per-service free trials that begin the moment you first enable the service, run for a fixed window, and then bill fully. They do not align with your 12-month clock.

Service Trial Watch-out
GuardDuty 30 days Bills per-event + per-GB after
Amazon Inspector 15 days Per-resource after
Amazon Redshift 2 months / 750 hrs dc2.large Cluster is expensive after
Amazon SageMaker 2 months Notebook + endpoint hours after
Macie 30 days Per-GB scanned after
QuickSight 30-day author trial Per-user/session after

The newer credit-based sign-up plan

For accounts created from mid-2025 onward, AWS layered a credit-based free plan on top: on sign-up you receive credits (a sign-up grant plus more for completing onboarding activities, on the order of up to $200 total), valid for a limited window (about 6 months), and you can operate in a “free account plan” that will not incur charges until you deliberately upgrade to a paid plan — at which point normal Free Tier + pay-as-you-go rules apply. Legacy accounts keep the classic 12-month tier. Because both models are in the wild, always check Billing → Free tier and Billing → Credits to see which you have and how much runway remains.

Model Who has it Guardrail Where to check
Classic 12-month + always-free Accounts before ~Jul 2025 Alerts only (no cap) Billing → Free tier
Credit-based free plan Newer accounts Free plan blocks charges until you upgrade Billing → Credits, Free tier

Free-tier usage alerts

Independent of Budgets, AWS emails you when you are forecast to exceed 85% of a 12-month/always-free limit. Turn it on once:

Setting Where Behaviour
Receive AWS Free Tier alerts Billing → Billing preferences → Alert preferences Emails at ~85% of a free-tier limit (actual or forecast)
Additional email address Same panel Sends to an ops inbox, not just the root email
Free tier usage report Billing → Free tier Table of each service vs its limit (actual + forecast)
# View current free-tier usage (needs the newer Free Tier API / console)
aws freetier get-free-tier-usage --region us-east-1 \
  --query 'freeTierUsages[].[service,usageType,actualUsageAmount,limit,forecastedUsageAmount]' \
  --output table

This is your first, zero-cost guardrail. It will not stop you crossing a limit — nothing does — but it means you find out at 85%, not on the invoice.

The Billing console and enabling Cost Explorer

Everything lives under Billing and Cost Management (the console link is usually your account-name menu → Billing and Cost Management). The pages you will use:

Page What it shows You’ll use it to
Home / Bills Month-to-date and past invoices by service/region See the actual charge
Payments / Credits Payment history, promotional credits balance Check remaining credits
Cost Explorer Interactive cost charts + forecast Analyse where money went
Budgets Cost/usage/RI/SP budgets + alerts Set the trip-wires
Budgets Reports Scheduled digests of budget status Weekly/monthly rollup
Cost Anomaly Detection Monitors + detected anomalies Catch spikes
Cost and Usage Reports / Data Exports CUR configuration → S3 Deep, resource-level analysis
Cost Categories Rules that group cost into buckets Map cost to teams/products
Cost allocation tags Activate tag keys for cost breakdown Slice CE/CUR by tag
Free tier Usage vs each free-tier limit Track the cliff
Billing preferences Alerts, invoice, credit-sharing toggles Opt in to alerts
Payment preferences Default payment method, currency Billing setup
Purchase orders PO management (enterprise) Invoicing

Two preferences unlock the alerting in this article. Set both once, in us-east-1 context:

Preference Location Unlocks
Receive CloudWatch billing alerts Billing preferences → Alert preferences The AWS/Billing EstimatedCharges metric in us-east-1
Receive AWS Free Tier alerts Billing preferences → Alert preferences Free-tier 85% emails

Enabling Cost Explorer

Cost Explorer is off until you enable it once. Go to Billing → Cost Explorer → Launch/Enable. The first enable back-fills roughly the last 24 hours of history and up to ~12–14 months of past data over the following day — so if you enable it and it looks empty, that is expected; come back tomorrow. There is no CLI to “enable” it (it is an account-level, one-time console action), but once enabled the Cost Explorer API (ce) works:

# After enabling in the console, confirm the ce API responds (each call = $0.01)
aws ce get-cost-and-usage \
  --time-period Start=2026-06-01,End=2026-07-01 \
  --granularity MONTHLY \
  --metrics "UnblendedCost" \
  --query 'ResultsByTime[].Total.UnblendedCost' --output table

Once enabled, you can also grant IAM principals ce:* (read) so a team can self-serve without root.

Cost Explorer in depth

Cost Explorer answers “where did the money go, and where is it heading?” It is a UI over the same data the CUR holds, minus per-resource detail unless you pay for it. Four levers control every view: filter, group by, granularity, and date range (with forecast and cost type on top).

Filters — narrow the population

You can filter a report by any of these dimensions (include or exclude), and stack multiple filters (AND across dimensions, OR within one):

Filter dimension Example value Surfaces
Service Amazon Relational Database Service Per-service spend
Linked account 1234-5678-9012 Which account in the org
Region ap-south-1 Regional spend (egress lives here)
Instance type t3.micro, m5.large Right-sizing candidates
Usage type NatGateway-Hours, DataTransfer-Out-Bytes The exact meter (culprit-hunting)
Usage type group EC2: Running Hours Coarser bucket of meters
Purchase option On-Demand / Reserved / Spot / Savings Plan Commitment coverage
API operation GetObject, PutItem Request-driven cost (S3/DynamoDB)
Tenancy Shared / Dedicated Dedicated-host waste
Availability Zone apse1-az1 Inter-AZ transfer
Platform Linux / Windows / RHEL Licensing cost
Tag Environment=prod, Team=payments Team/project cost (must be activated)
Cost category Product=Checkout Business-mapped cost
Charge type Usage / Tax / Credit / Refund / RI fee Separate credits from real usage
Billing entity AWS / AWS Marketplace Marketplace SaaS charges
Legal entity AWS Inc / AWS EMEA Multi-entity invoicing
Record type Usage / Fee / Enterprise Discount Discount visibility
Resource i-0abc… (hourly opt-in) Per-resource cost (paid feature)

Group by — pivot the axis

The single most useful skill: group by Service for the what, group by Usage type for the culprit, group by Tag for the who, group by Linked account for the where.

Group by Best question it answers
Service “Which service costs the most?” (default starting point)
Usage type “What exact meter is billing?” (finds NAT/egress)
Usage type group “Compute vs storage vs transfer at a glance”
Linked account “Which team’s account is the spend?”
Region “Where is my egress and regional cost?”
Instance type “Am I over-provisioned on a size?”
Purchase option “How much is On-Demand vs committed?”
API operation “Which call pattern drives S3/DDB cost?”
Tag key “Cost per team/project/environment”
Cost category “Cost per business unit/product”
Charge type “Usage vs tax vs credits”
Legal entity / Billing entity “Marketplace vs AWS, per invoicing entity”

Only one primary group-by applies at a time in the UI; combine with filters to drill (e.g. filter Service = EC2, group by Usage type → see BoxUsage vs NatGateway vs DataTransfer).

Granularity and date range

Setting Options Notes
Granularity Monthly / Daily / Hourly Hourly needs the paid “hourly & resource-level” opt-in
Historical range Up to ~14 months back Older data isn’t in CE (use CUR/S3)
Forecast range Up to 12 months forward Confidence band widens with distance
Resource-level On/off (paid) Adds per-resource rows; ~$0.01/1,000 usage records/mo

Cost types — the number is not one number

The same usage has several legitimate “costs” depending on how you treat commitments and org sharing. Pick deliberately:

Cost type Definition Use it for
Unblended The actual rate on each line at time of use Matching the invoice, default
Amortized Upfront RI/SP fees spread across the term True run-rate with commitments
Blended Averaged rate across an org’s accounts Org-wide fairness view
Net unblended Unblended after discounts (EDP/credits) Real out-of-pocket
Net amortized Amortized after discounts Discounted run-rate

Forecast

Cost Explorer’s forecast uses your history to project month-end (and up to 12 months out) with an 80% confidence interval. It needs a baseline — on a brand-new account or right after a big architecture change the forecast is wildly off, because there is little or contradictory history. Forecast improves after a few weeks of steady usage.

RI and Savings Plans reports

If you buy commitments, Cost Explorer ships purpose-built reports:

Report Answers Target
RI Utilization “Am I using the reservations I bought?” ≥ 95%
RI Coverage “What % of eligible usage is covered by RIs?” Set to your risk appetite
Savings Plans Utilization “Am I using my SP commitment?” ≥ 95%
Savings Plans Coverage “What % of eligible usage SP covers?” Balance savings vs flexibility
RI/SP Recommendations “What should I buy, at what term/payment?” 7/30/60-day lookback

Saved reports and defaults

Cost Explorer ships default reports (Monthly costs by service, Daily costs, Monthly costs by linked account, RI/SP utilization & coverage, etc.) and lets you save any customised view. Saved reports are how a team standardises “the three views we check every Monday.”

Default report Shows
Monthly costs by service Stacked bar per service
Monthly costs by linked account Per-account rollup
Daily costs Day-by-day trend + forecast
RI Utilization / Coverage Commitment health
Savings Plans Utilization / Coverage Commitment health
Monthly EC2 running hours Compute usage trend
# Cost Explorer itself is enabled in the console (one-time), but you can
# codify an "anomaly monitor" and cost categories that shape CE. Example: a
# cost category that maps usage into business buckets Cost Explorer can group by.
resource "aws_ce_cost_category" "product" {
  name         = "Product"
  rule_version = "CostCategoryExpression.v1"

  rule {
    value = "Checkout"
    rule {
      tags {
        key           = "Product"
        values        = ["checkout"]
        match_options = ["EQUALS"]
      }
    }
  }
  rule {
    value = "Platform"
    rule {
      dimension {
        key           = "LINKED_ACCOUNT"
        values        = ["111122223333"]
        match_options = ["EQUALS"]
      }
    }
  }
}

AWS Budgets: cost, usage, RI, Savings Plans, and budget actions

A Budget is a target plus one or more alert thresholds that notify (and optionally act) when crossed. This is your primary trip-wire.

Budget types

Type Tracks Typical use
Cost budget Dollars spent “Alert if this account > $10/mo”
Usage budget A usage metric (hours, GB, requests) “Alert if EC2 hours > 750 (free-tier cliff)”
RI utilization budget % of RIs used “Alert if RI utilization < 90%”
RI coverage budget % of usage covered by RIs “Alert if coverage < 70%”
Savings Plans utilization % of SP commitment used “Alert if SP utilization < 90%”
Savings Plans coverage % of eligible usage SP covers “Alert if coverage < 70%”

Budget parameters

Parameter Values Notes
Period Daily / Monthly / Quarterly / Annually Match how you reason about spend
Amount Fixed, or auto-adjusting (avg of last N periods / forecast) Auto-adjust tracks a moving baseline
Start/effective date Date Back-dated budgets can evaluate prior period
Scope / filters Service, account, tag, region, purchase option, etc. Same dimensions as Cost Explorer
Cost types included Refunds, credits, taxes, upfront, recurring, other-subscription, support, discounts Toggle what “counts”
Aggregation Unblended / Amortized / Blended Match your accounting view
Alerts Up to 5 per budget Each has %, actual/forecast, recipients
Notifications Up to 10 email addresses + SNS topic + Chatbot Fan-out targets

Actual vs forecasted — the distinction that trips everyone

Alert basis Fires when Pro Con
Actual Real spend crosses the % Never a false alarm Only warns after money is spent
Forecasted Projected month-end crosses the % Early warning Needs history; noisy on new accounts

The standard pattern for a small account: actual at 80% and 100%, plus forecasted at 100% once you have a few weeks of history. On a brand-new account, skip forecasted until the forecast stabilises, or it will scream on day two.

Budget actions — make it actually stop

A budget action runs automatically (or after manual approval) when a threshold is crossed. This is the closest AWS gets to a spending cap.

Action type Effect Applies to
Apply IAM policy Attach a restrictive (usually Deny) policy Users / groups / roles
Apply SCP Attach a Service Control Policy OUs / member accounts (org)
Target EC2/RDS instances Stop specified running instances Tagged EC2 / RDS

Actions can require approval (someone clicks “execute”) or run automatically. Use them carefully — an auto-applied Deny SCP that fires on a forecast blip can lock a team out of the account. Prefer approval-required for anything destructive.

Notification targets

Target How Good for
Email Up to 10 addresses on the alert Simple, direct
SNS topic Budgets publishes to the topic Fan-out to Lambda/Slack/PagerDuty
AWS Chatbot Via SNS → Chatbot → Slack/Teams ChatOps

For SNS, the topic access policy must allow budgets.amazonaws.com to SNS:Publish — the number-one reason a budget alert never arrives is a missing topic policy or an unconfirmed subscription.

Budgets pricing

Item Cost
First 2 budgets Free
Each additional budget $0.02 / budget / day
Budget actions No extra charge for the action feature
Budget alerts via SNS/email SNS/email standard (free tier covers small volume)

CLI and Terraform

# Create a $10/month cost budget (JSON files shown in the lab section)
aws budgets create-budget \
  --account-id 111122223333 \
  --budget file://budget.json \
  --notifications-with-subscribers file://notifications.json
resource "aws_budgets_budget" "monthly_cost" {
  name         = "monthly-cost-10usd"
  budget_type  = "COST"
  limit_amount = "10"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  cost_types {
    include_credit = false
    include_refund = false
    include_tax    = true
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_sns_topic_arns  = [aws_sns_topic.budget_alerts.arn]
    subscriber_email_addresses = ["ops@example.com"]
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_sns_topic_arns  = [aws_sns_topic.budget_alerts.arn]
  }
}

The CloudWatch billing alarm

Before Budgets existed, this was the cost alert, and it is still the cheapest near-real-time signal. AWS publishes a single running metric, EstimatedCharges in the AWS/Billing namespace — but only to CloudWatch in us-east-1, and only after you opt in to “Receive CloudWatch billing alerts.”

Property Value
Namespace AWS/Billing
Metric EstimatedCharges
Dimensions Currency=USD; optionally ServiceName=…, LinkedAccount=…
Region us-east-1 ONLY (global billing data is published there)
Update frequency ~every 6 hours
Prerequisite Billing preferences → Receive Billing Alerts = on
Statistic Maximum (it’s a cumulative month-to-date value)
Period 21600 s (6 hrs) or 86400 s (1 day)
Value Cumulative month-to-date USD; resets at month start
# 1) Confirm the metric exists (only works in us-east-1, after opt-in)
aws cloudwatch list-metrics --namespace "AWS/Billing" \
  --metric-name EstimatedCharges --region us-east-1

# 2) Create an alarm that fires when month-to-date > $10
aws cloudwatch put-metric-alarm --region us-east-1 \
  --alarm-name "billing-over-10-usd" \
  --namespace "AWS/Billing" --metric-name EstimatedCharges \
  --dimensions Name=Currency,Value=USD \
  --statistic Maximum --period 21600 --evaluation-periods 1 \
  --threshold 10 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:us-east-1:111122223333:billing-alerts
resource "aws_cloudwatch_metric_alarm" "billing" {
  provider            = aws.use1              # MUST be us-east-1
  alarm_name          = "billing-over-10-usd"
  namespace           = "AWS/Billing"
  metric_name         = "EstimatedCharges"
  dimensions          = { Currency = "USD" }
  statistic           = "Maximum"
  period              = 21600
  evaluation_periods  = 1
  threshold           = 10
  comparison_operator = "GreaterThanThreshold"
  treat_missing_data  = "notBreaching"
  alarm_actions       = [aws_sns_topic.budget_alerts.arn]
}

Budgets vs the billing alarm: Budgets is richer (usage/RI/SP, forecast, filters, actions, per-service) and is what you build going forward; the CloudWatch alarm is a dead-simple total-dollar trip-wire that many teams keep as a belt-and-braces backstop.

Cost Anomaly Detection

Budgets and alarms fire on thresholds you guessed. Cost Anomaly Detection (CAD) is different: it uses machine learning to learn your normal spend pattern per service and alerts when reality deviates — catching the spike you did not think to threshold. It is free.

Monitors — what CAD watches

Monitor type Watches Best for
AWS services (recommended) Every service, evaluated individually Default; catches any service spiking
Linked account A specific member account Per-team accountability
Cost category A cost-category value Per-product/business unit
Cost allocation tag A specific tag key/value Per-project

Alert subscriptions

Setting Options Notes
Threshold Absolute $ or % impact e.g. alert if anomaly > $50 or > 20%
Frequency Individual / Daily digest / Weekly digest Individual for real spikes; digest for noise
Delivery Email or SNS topic SNS for fan-out to Slack
Recipients Emails / SNS ARN Route to the right team
# A monitor over all AWS services
aws ce create-anomaly-monitor --anomaly-monitor \
  '{"MonitorName":"all-services","MonitorType":"DIMENSIONAL","MonitorDimension":"SERVICE"}'

# A subscription that emails when an anomaly's impact exceeds $50
aws ce create-anomaly-subscription --anomaly-subscription '{
  "SubscriptionName":"anomalies-over-50",
  "MonitorArnList":["arn:aws:ce::111122223333:anomalymonitor/…"],
  "Frequency":"IMMEDIATE",
  "Subscribers":[{"Type":"EMAIL","Address":"ops@example.com"}],
  "ThresholdExpression":{"Dimensions":{"Key":"ANOMALY_TOTAL_IMPACT_ABSOLUTE","Values":["50"],"MatchOptions":["GREATER_THAN_OR_EQUAL"]}}
}'
resource "aws_ce_anomaly_monitor" "services" {
  name              = "all-services"
  monitor_type      = "DIMENSIONAL"
  monitor_dimension = "SERVICE"
}

resource "aws_ce_anomaly_subscription" "over_50" {
  name      = "anomalies-over-50"
  frequency = "IMMEDIATE"
  monitor_arn_list = [aws_ce_anomaly_monitor.services.arn]

  subscriber {
    type    = "EMAIL"
    address = "ops@example.com"
  }

  threshold_expression {
    dimension {
      key           = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
      values        = ["50"]
      match_options = ["GREATER_THAN_OR_EQUAL"]
    }
  }
}

CAD needs about 10 days of history to build a baseline. Deep-dive triage of a fired anomaly (root-causing the exact usage type and resource) is its own discipline — see the companion piece on cost-spike anomaly-detection troubleshooting.

Consolidated billing across an organization

If you use AWS Organizations, the management (payer) account receives one consolidated bill for all member (linked) accounts. This is not just convenience — it changes the numbers.

Behaviour How it works in an org
Single invoice Payer account pays; members show as line items
Volume tiering (blended) Usage aggregates across accounts to reach cheaper tiers (e.g. S3, data transfer), then blends
Free Tier One Free Tier applies across the whole org, not per account
RI / Savings Plans sharing Unused commitments in one account can cover eligible usage in another (toggle per account)
Credits sharing Promotional credits can be shared org-wide (toggle in preferences)
Cost Explorer / Budgets Payer sees all accounts; members see only themselves (unless granted)
CUR The payer’s CUR contains every account’s line items
Cost allocation tags Activated once at the payer, apply org-wide

The Free-Tier rule surprises people: spin up ten member accounts thinking each gets 750 free EC2 hours, and you get 750 total, not 7,500. Turn off RI/SP sharing for an account when a team wants to see its own un-subsidised cost. Governance of member accounts (SCPs, guardrails, account vending) is covered in AWS Control Tower, Guardrails and the Multi-Account Foundation.

The Cost and Usage Report (CUR)

Cost Explorer is a UI with limits (14 months, coarse resource detail). When you need every line item, hourly, at resource granularity, queryable in SQL, you enable the Cost and Usage Report — the most detailed billing data AWS produces — delivered to your S3 bucket (now via Data Exports / CUR 2.0).

Config option Values Notes
Granularity Hourly / Daily / Monthly Hourly for precise spike analysis
Format CSV (GZIP) / Parquet Parquet for Athena/QuickSight
Include resource IDs On/off On = per-resource rows (bigger files)
Split cost allocation data On/off Per-pod/task cost for ECS/EKS
Time granularity of versions Overwrite / Create new Overwrite keeps one current file set
Refresh Up to ~3×/day Back-fills estimates until finalised
S3 bucket Your bucket + prefix Needs a bucket policy for billingreports.amazonaws.com
Athena/Redshift/QuickSight Integration templates Query without moving data

CUR vs Cost Explorer, at a glance:

Dimension Cost Explorer CUR
Detail Service/usage-type, tag, account Every line item, per resource, per hour
History ~14 months Unlimited (you keep the S3 data)
Query UI + ce API SQL (Athena/Redshift), any BI tool
Latency Up to ~24h Up to ~24h, refreshed
Cost UI free; API $0.01/call S3 storage + Athena scan
Best for Quick “where did it go” Deep audits, chargeback, big-data analysis
# List existing CUR definitions (must run in us-east-1)
aws cur describe-report-definitions --region us-east-1

# Create a Parquet, hourly, resource-level CUR (report def JSON abbreviated)
aws cur put-report-definition --region us-east-1 --report-definition '{
  "ReportName":"kloudvin-cur",
  "TimeUnit":"HOURLY",
  "Format":"Parquet",
  "Compression":"Parquet",
  "AdditionalSchemaElements":["RESOURCES"],
  "S3Bucket":"kloudvin-cur-111122223333",
  "S3Prefix":"cur",
  "S3Region":"us-east-1",
  "ReportVersioning":"OVERWRITE_REPORT"
}'
resource "aws_cur_report_definition" "cur" {
  provider                   = aws.use1
  report_name                = "kloudvin-cur"
  time_unit                  = "HOURLY"
  format                     = "Parquet"
  compression                = "Parquet"
  additional_schema_elements = ["RESOURCES"]
  s3_bucket                  = aws_s3_bucket.cur.id
  s3_region                  = "us-east-1"
  s3_prefix                  = "cur"
  additional_artifacts       = ["ATHENA"]
  report_versioning          = "OVERWRITE_REPORT"
}

The biggest surprise-bill culprits

Most shock bills come from a short list of resources that bill 24×7 regardless of app traffic, or that meter data you did not realise moved. Learn this table and the Cost Explorer filter that finds each — it is the single most valuable page here.

Culprit Why it surprises Rough rate (us-east-1) Find it in Cost Explorer
NAT gateway (hours) Per-AZ, 24×7, even idle ~$0.045/hr each Filter Usage type *NatGateway-Hours
NAT gateway (data) $/GB on top of hours ~$0.045/GB Usage type *NatGateway-Bytes
Public IPv4 address Now charged even when attached ~$0.005/hr each Usage type PublicIPv4:InUseAddress
Idle Elastic IP Unattached EIP billed ~$0.005/hr Usage type ElasticIP:IdleAddress
Inter-AZ data transfer Crossing AZs isn’t free ~$0.01/GB each way Group by Region + Usage type DataTransfer-Regional-Bytes
Internet egress Out to internet after 100 GB free ~$0.09/GB (to 10 TB) Usage type DataTransfer-Out-Bytes
Unattached EBS volume Detached gp3 still bills ~$0.08/GB-mo Service EBS, group by Usage type VolumeUsage
EBS snapshots Accumulate forever ~$0.05/GB-mo Usage type SnapshotUsage
RDS running/Multi-AZ Multi-AZ doubles the instance Instance + storage Service RDS, group by Instance type
Idle load balancer ALB/NLB bill per-hour + LCU ~$0.0225/hr + LCU Service ELB, group by Usage type
CloudWatch Logs Verbose logging = ingestion $ ~$0.50/GB ingest Service CloudWatch, Usage type *DataProcessing-Bytes
EKS control plane $0.10/hr per cluster, even empty ~$0.10/hr Service EKS
VPC endpoints Interface endpoints bill per-hour + data ~$0.01/hr each Service EC2-Other, Usage type VpcEndpoint*
Provisioned DynamoDB / Kinesis Provisioned capacity/shards bill idle Varies Service DynamoDB / Kinesis

Data transfer pricing (the invisible meter)

Path Rate (typical) Free?
Into AWS from internet $0.00 Free
Out to internet ~$0.09/GB (first 10 TB) First 100 GB/mo free (aggregate)
Same AZ, private IP $0.00 Free
Cross-AZ, same region ~$0.01/GB each direction No
Cross-region ~$0.02/GB (varies by pair) No
Via NAT gateway +~$0.045/GB processed On top of egress
Via CloudFront to internet ~$0.085/GB (first 10 TB) 1 TB/mo always free
S3 to same-region EC2 $0.00 Free
S3 to internet ~$0.09/GB 100 GB/mo aggregate free
Inter-region VPC peering ~$0.01–0.02/GB No

The lesson: egress and cross-AZ transfer are the meters you cannot see in the console’s resource list. Cost Explorer grouped by Usage type is the only place they become obvious.

Architecture at a glance

The diagram traces one dollar of cost from the resource that spends it to the human who gets alerted, left to right, and marks the six points where a beginner’s setup breaks. Start at USAGE: your running resources (EC2, RDS, EBS, ALB) meter continuously, and the classic culprit — a NAT gateway at ~$0.045/hr plus ~$0.045/GB, per AZ (badge 1) — bills 24×7 whether or not your app is busy. That usage flows into COST DATA: Cost Explorer renders it in the UI after a ~24-hour lag and a one-time enable (badge 2), while the CUR lands hourly Parquet line items in an S3 bucket for deep, per-resource SQL analysis (badge 3).

Three MONITORS watch that data: AWS Budgets evaluates both actual and forecasted spend against your thresholds (badge 4), Cost Anomaly Detection compares reality to an ML baseline, and a CloudWatch billing alarm on EstimatedCharges — which exists only in us-east-1 after you opt in (badge 5). When any of them crosses its line, the BREACH → FAN-OUT stage publishes to a single SNS topic — but only if the subscription is confirmed and the topic policy lets budgets.amazonaws.com publish (badge 6). Finally, NOTIFY + ACT delivers the alert to an email/Slack inbox and can trigger an automated budget action (apply a restrictive SCP/IAM policy or stop tagged EC2/RDS) — the closest AWS gets to a hard cap. Read the legend as symptom · how to confirm · fix for each numbered failure point.

AWS cost-control pipeline showing running resources including a NAT gateway metering into a cost-data layer of Cost Explorer and a Cost and Usage Report in an S3 bucket, watched by three monitors — AWS Budgets evaluating actual and forecasted spend, Cost Anomaly Detection on an ML baseline, and a CloudWatch billing alarm on EstimatedCharges that only exists in us-east-1 — with a threshold breach fanning out through one confirmed SNS topic to email/Slack and an optional automated budget action that applies a restrictive SCP or stops instances, annotated with six numbered failure-point badges for the NAT-gateway surprise, Cost Explorer 24-hour lag, CUR-to-S3 depth, actual-vs-forecast budgets, the us-east-1-only billing metric, and SNS subscription confirmation

Real-world scenario

FrameHive, a five-person video-tooling startup in Bengaluru, ran everything in one AWS account on a personal credit card. Their architect had spread the app across three AZs “for resilience,” following a reference architecture that placed a NAT gateway in each private subnet. Month one, the founder — who had signed up believing the t3.micro tier meant “basically free” — got a ₹14,800 (~$178) bill and panicked, assuming they had been hacked.

They had not. The triage, using exactly the tools in this article, took twenty minutes. First, Cost Explorer → group by Service showed the money split roughly: EC2-Other $71, EC2 $9, RDS $58, ELB $16, CloudWatch $12, everything else <$12. “EC2-Other” is where AWS files NAT gateways, EBS, and data transfer — so the founder re-ran with group by Usage type, which is the move that cracks every one of these. The top rows were unmistakable: NatGateway-Hours $97 across three gateways, NatGateway-Bytes $14, EBS:VolumeUsage.gp3 $11 on two volumes attached to instances they had terminated (the volumes were not deleted on termination), and PublicIPv4:InUseAddress $8 for addresses on resources they forgot they had.

The fixes were structural and free-to-cheap. They deleted two NAT gateways and kept one (dev traffic did not need per-AZ NAT), saving ~$65/month, and replaced S3 and DynamoDB access with gateway VPC endpoints (free) so that traffic never touched NAT at all. They deleted the two orphaned gp3 volumes and set DeleteOnTermination=true on future instances. They released the idle Elastic IPs. RDS was legitimately needed but was Multi-AZ on a db.t3.medium for a dev database — they dropped it to Single-AZ db.t3.micro (inside the 12-month tier), cutting it to near-zero.

Then they built the guardrails so this could never recur silently. A $50/month cost Budget with actual alerts at 80% and 100% and a forecasted alert at 100%, all to an SNS topic that fanned to the whole team’s Slack via AWS Chatbot. A CloudWatch billing alarm at $30 in us-east-1 as a backstop. Cost Anomaly Detection with an all-services monitor and a $25 impact threshold. And free-tier usage alerts on, so the next time they approached 750 EC2 hours they would know at 85%. The next month’s bill was ₹1,900 (~$23) — and when a teammate later left a load-test ALB running over a weekend, the anomaly alert fired on Saturday morning, not on the invoice. Total cost of all four guardrails: under $1/month.

Advantages and disadvantages

Advantages Disadvantages
Guardrails are free or cost cents Nothing is a true real-time hard cap
Set once, protect forever All signals lag hours (up to ~24h)
Cost Explorer needs no setup after enable CE resource-level detail is a paid opt-in
Budgets support actual + forecast + actions Forecast is unreliable on new/changed accounts
Anomaly Detection needs zero thresholds CAD needs ~10 days to learn a baseline
CUR gives unlimited, SQL-queryable depth CUR needs S3 + Athena setup to be useful
Works across an org with one bill Free Tier is org-wide, not per-account
Budget actions can auto-restrict spend Auto-actions can lock a team out if misconfigured

Cost Explorer + Budgets + free-tier alerts cover the 90% case for a beginner at essentially no cost. Reach for the CUR and Cost Categories only when a single account’s spend gets big enough to need chargeback and per-resource audits. Reserve budget actions (especially auto-apply SCP/IAM) for accounts where a runaway is genuinely dangerous, and prefer approval-required to avoid self-inflicted lockouts.

Hands-on lab

You will set a free-tier alert, create a $10 monthly cost Budget with 80% and 100% SNS alerts, and add a CloudWatch billing alarm — via Console, aws CLI, and Terraform — then tear it all down. Nothing here launches a paid resource; SNS and the first two budgets and ten alarms are within free limits.

⚠️ Cost note: 3rd+ budgets cost $0.02/day; the first two are free. SNS email and 10 CloudWatch alarms are free-tier. This lab stays free.

Part A — Turn on the two billing preferences (Console)

  1. Sign in and open Billing and Cost Management. Ensure the console Region shows us-east-1 for billing actions (top-right selector) — billing data is global but published in us-east-1.
  2. Go to Billing preferences → Alert preferences → Edit.
  3. Tick Receive AWS Free Tier alerts and enter an ops email in Additional email address.
  4. Tick Receive CloudWatch billing alerts (this enables the EstimatedCharges metric).
  5. Save. Expected: both toggles show enabled; the CloudWatch metric appears in us-east-1 within a few hours.

Part B — Create the SNS topic and confirm a subscription (CLI)

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# Create the topic in us-east-1 (so Budgets + the billing alarm can share it)
TOPIC_ARN=$(aws sns create-topic --name billing-alerts --region us-east-1 \
  --query TopicArn --output text)
echo "$TOPIC_ARN"   # arn:aws:sns:us-east-1:<acct>:billing-alerts

# Subscribe your email; AWS emails a confirmation link you MUST click
aws sns subscribe --topic-arn "$TOPIC_ARN" --protocol email \
  --notification-endpoint ops@example.com --region us-east-1

# Verify: after clicking the link, this should show a real SubscriptionArn
aws sns list-subscriptions-by-topic --topic-arn "$TOPIC_ARN" --region us-east-1 \
  --query 'Subscriptions[].[Endpoint,SubscriptionArn]' --output table

Expected: before you click the email link, SubscriptionArn is PendingConfirmation; after, it is a real ARN. An unconfirmed subscription receives nothing — this is the most common “my alert never came” bug.

Now allow Budgets to publish to the topic (attach an access policy):

aws sns set-topic-attributes --region us-east-1 --topic-arn "$TOPIC_ARN" \
  --attribute-name Policy --attribute-value "{
    \"Version\":\"2012-10-17\",
    \"Statement\":[{
      \"Sid\":\"AllowBudgets\",
      \"Effect\":\"Allow\",
      \"Principal\":{\"Service\":\"budgets.amazonaws.com\"},
      \"Action\":\"SNS:Publish\",
      \"Resource\":\"$TOPIC_ARN\"
    }]
  }"

Part C — Create the $10 monthly Budget with 80%/100% alerts (CLI)

Create budget.json:

{
  "BudgetName": "monthly-cost-10usd",
  "BudgetLimit": { "Amount": "10", "Unit": "USD" },
  "TimeUnit": "MONTHLY",
  "BudgetType": "COST"
}

Create notifications.json (80% actual + 100% forecasted, both to SNS + email):

[
  {
    "Notification": {
      "NotificationType": "ACTUAL",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 80,
      "ThresholdType": "PERCENTAGE"
    },
    "Subscribers": [
      { "SubscriptionType": "SNS", "Address": "arn:aws:sns:us-east-1:111122223333:billing-alerts" },
      { "SubscriptionType": "EMAIL", "Address": "ops@example.com" }
    ]
  },
  {
    "Notification": {
      "NotificationType": "FORECASTED",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 100,
      "ThresholdType": "PERCENTAGE"
    },
    "Subscribers": [
      { "SubscriptionType": "SNS", "Address": "arn:aws:sns:us-east-1:111122223333:billing-alerts" }
    ]
  }
]
aws budgets create-budget \
  --account-id "$ACCOUNT_ID" \
  --budget file://budget.json \
  --notifications-with-subscribers file://notifications.json

# Verify
aws budgets describe-budget --account-id "$ACCOUNT_ID" \
  --budget-name monthly-cost-10usd \
  --query 'Budget.{Name:BudgetName,Limit:BudgetLimit.Amount,Type:BudgetType}' \
  --output table

Expected: the budget lists with a $10 limit; Budgets → your budget in the console shows both alerts. (Replace 111122223333 with your real account ID in the JSON.)

Part D — Add the CloudWatch billing alarm (CLI)

aws cloudwatch put-metric-alarm --region us-east-1 \
  --alarm-name "billing-over-10-usd" \
  --namespace "AWS/Billing" --metric-name EstimatedCharges \
  --dimensions Name=Currency,Value=USD \
  --statistic Maximum --period 21600 --evaluation-periods 1 \
  --threshold 10 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions "$TOPIC_ARN"

# Verify (state may be INSUFFICIENT_DATA until the metric populates ~6h)
aws cloudwatch describe-alarms --region us-east-1 \
  --alarm-names billing-over-10-usd \
  --query 'MetricAlarms[].[AlarmName,StateValue]' --output table

Expected: the alarm exists; state is INSUFFICIENT_DATA until EstimatedCharges publishes (up to ~6h after opt-in), then OK.

Part E — Everything as Terraform (IaC)

terraform {
  required_providers { aws = { source = "hashicorp/aws" } }
}

# Billing metric + CUR live in us-east-1 — use an aliased provider
provider "aws" { region = "ap-south-1" }
provider "aws" { alias = "use1"  region = "us-east-1" }

data "aws_caller_identity" "me" {}

resource "aws_sns_topic" "budget_alerts" {
  provider = aws.use1
  name     = "billing-alerts"
}

resource "aws_sns_topic_policy" "allow_budgets" {
  provider = aws.use1
  arn      = aws_sns_topic.budget_alerts.arn
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect    = "Allow",
      Principal = { Service = "budgets.amazonaws.com" },
      Action    = "SNS:Publish",
      Resource  = aws_sns_topic.budget_alerts.arn
    }]
  })
}

resource "aws_sns_topic_subscription" "email" {
  provider  = aws.use1
  topic_arn = aws_sns_topic.budget_alerts.arn
  protocol  = "email"
  endpoint  = "ops@example.com"   # confirm the link in your inbox
}

resource "aws_budgets_budget" "monthly_cost" {
  name         = "monthly-cost-10usd"
  budget_type  = "COST"
  limit_amount = "10"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_sns_topic_arns  = [aws_sns_topic.budget_alerts.arn]
    subscriber_email_addresses = ["ops@example.com"]
  }
  notification {
    comparison_operator       = "GREATER_THAN"
    threshold                 = 100
    threshold_type            = "PERCENTAGE"
    notification_type         = "FORECASTED"
    subscriber_sns_topic_arns = [aws_sns_topic.budget_alerts.arn]
  }
}

resource "aws_cloudwatch_metric_alarm" "billing" {
  provider            = aws.use1
  alarm_name          = "billing-over-10-usd"
  namespace           = "AWS/Billing"
  metric_name         = "EstimatedCharges"
  dimensions          = { Currency = "USD" }
  statistic           = "Maximum"
  period              = 21600
  evaluation_periods  = 1
  threshold           = 10
  comparison_operator = "GreaterThanThreshold"
  treat_missing_data  = "notBreaching"
  alarm_actions       = [aws_sns_topic.budget_alerts.arn]
}
terraform init && terraform apply   # confirm the SNS email link afterwards

Part F — Teardown

# CLI-created resources
aws cloudwatch delete-alarms --alarm-names billing-over-10-usd --region us-east-1
aws budgets delete-budget --account-id "$ACCOUNT_ID" --budget-name monthly-cost-10usd
aws sns delete-topic --topic-arn "$TOPIC_ARN" --region us-east-1

# Terraform-created resources
terraform destroy

Expected: describe-alarms and describe-budget return empty/NotFound. Leave Receive Billing Alerts / Free Tier alerts on — they cost nothing and you want them permanently.

Common mistakes & troubleshooting

This is the playbook — bookmark it. First the scannable table, then the nastiest failures in full.

# Symptom Root cause Confirm (exact cmd / console path) Fix
1 Budget/alarm alert never arrives by email SNS subscription unconfirmed aws sns list-subscriptions-by-topicSubscriptionArn = PendingConfirmation Click the confirmation link in the email
2 Budget crosses threshold, SNS silent Topic policy blocks Budgets aws sns get-topic-attributes → no budgets.amazonaws.com Allow Add SNS:Publish allow for budgets.amazonaws.com
3 Cost Explorer shows nothing / empty Just enabled; ~24h back-fill Billing → Cost Explorer shows an “enabling” banner Wait ~24h; it back-fills history
4 Billing alarm stuck INSUFFICIENT_DATA Metric not in this Region, or opt-in off aws cloudwatch list-metrics --namespace AWS/Billing --region us-east-1 empty Create alarm in us-east-1; enable Receive Billing Alerts
5 No AWS/Billing metric anywhere “Receive Billing Alerts” never enabled Billing preferences → Alert preferences unchecked Enable it; wait ~6h for first datapoint
6 Forecast alert fires on day 2 of a new account Forecast has no baseline Budget shows huge forecast vs tiny actual Use ACTUAL thresholds early; add FORECASTED later
7 Tag missing from Cost Explorer group-by Tag not activated as cost-allocation tag Billing → Cost allocation tags → key Inactive Activate the key; wait ~24h for it to populate
8 “AccessDenied” opening Bills/Cost Explorer IAM principal lacks billing permissions Error names ce: / aws-portal: / budgets: action Attach billing policy; management-acct may need to allow IAM billing access
9 Free Tier ended, bill jumped, no warning 12-month tier expired silently Billing → Free tier shows expired; charges at full rate Set free-tier alert + a small $ budget as backstop
10 Member accounts each expected 750 free EC2 hrs Free Tier is org-wide, not per-account One org = one Free Tier allowance Budget per account; don’t rely on per-account free tier
11 ce get-cost-and-usage returns DataUnavailableException Cost Explorer not enabled on the account Billing → Cost Explorer not launched Enable Cost Explorer once in the console
12 NAT/egress cost invisible in resource console It’s a usage-type meter, not a listed resource Cost Explorer group by Usage type shows NatGateway-* Delete extra NAT; use VPC endpoints; review egress
13 Anomaly Detection never alerts on a real spike No alert subscription, or threshold too high Cost Anomaly Detection → subscription list empty / high $ Add a subscription with a sensible $/% threshold
14 Budget action locked a team out of the account Auto-apply Deny SCP fired on a forecast blip Budget → action history shows auto-execute Set actions to approval-required; scope the policy
15 Terraform aws_cloudwatch_metric_alarm never fires Alarm created in the wrong region terraform state show → provider not us-east-1 Pin the aliased us-east-1 provider on the alarm

The three that bite hardest, in full:

1 & 2 — the alert that never comes. The overwhelmingly common failure is a silent SNS chain. An email subscription stays PendingConfirmation until someone clicks the AWS confirmation email, and an unconfirmed subscription receives nothing — no error, just silence. Separately, Budgets publishes as the service principal budgets.amazonaws.com, so the topic’s access policy must explicitly allow it to SNS:Publish; without that, the budget evaluates and “fires” but the publish is denied. Confirm both: aws sns list-subscriptions-by-topic --topic-arn <arn> (look for a real ARN, not PendingConfirmation) and aws sns get-topic-attributes --topic-arn <arn> (look for the budgets.amazonaws.com Allow). Fix by clicking the link and attaching the policy from Part B. Test end-to-end with aws sns publish --topic-arn <arn> --message test — if that email arrives, the chain works and the problem is upstream in the budget config.

4 & 5 — the billing alarm in the wrong Region. AWS/Billing EstimatedCharges is published only to us-east-1, because AWS treats billing as a global service homed there. Create the alarm in ap-south-1 and it will sit in INSUFFICIENT_DATA forever, because the metric does not exist in that Region. And even in us-east-1, the metric only appears after you enable Billing preferences → Receive Billing Alerts, and then only after the next ~6-hour publish. Confirm with aws cloudwatch list-metrics --namespace "AWS/Billing" --region us-east-1 — an empty result means either wrong Region or opt-in off. This is the single most common CloudWatch-billing-alarm mistake, and every Terraform config must pin an aliased us-east-1 provider on the alarm resource.

6 — the forecast that screams on a new account. Cost Explorer and Budgets forecast month-end from history. On a brand-new account, or the month after a big architecture change, there is little or contradictory history, so the forecast can project an absurd number and a FORECASTED alert fires immediately. This is expected, not a bug. Use ACTUAL thresholds (50/80/100%) for the first few weeks, then add a FORECASTED 100% alert once the daily trend has stabilised. The same caveat applies to auto-adjusting budgets: give them a couple of periods of history before trusting the baseline.

CLI error / status reference

Code / status Where Meaning Fix
DataUnavailableException ce API Cost Explorer not enabled / data not ready Enable CE; wait ~24h
AccessDeniedException ce / budgets Principal lacks billing IAM permissions Attach ce:*/budgets:*; allow IAM billing access
PendingConfirmation SNS subscription Email link not clicked Click the confirmation email
AuthorizationError sns publish from Budgets Topic policy denies the service Allow budgets.amazonaws.com SNS:Publish
INSUFFICIENT_DATA CloudWatch alarm No datapoints yet / wrong Region Use us-east-1; enable billing alerts; wait ~6h
ValidationException (budget) budgets Bad JSON (e.g. missing ThresholdType) Fix the notification JSON schema
ThrottlingException ce API Too many CE API calls Back off; each call also costs $0.01
ReportLimitExceeded cur API Too many CUR definitions Delete an old report definition
NotFoundException budgets describe-budget Wrong name/account Check --budget-name and --account-id
InvalidParameterValue (region) cloudwatch billing Alarm in non-us-east-1 Recreate in us-east-1

Decision table — “which control do I reach for?”

If you want to… Use Why
Know where the money went Cost Explorer (group by usage type) Fastest visual breakdown
Get warned before overspend Budgets (forecasted) Projects month-end
Get warned after overspend Budgets (actual) / billing alarm Never a false alarm
Catch a spike you didn’t threshold Cost Anomaly Detection Learns the baseline
Analyse per-resource, per-hour CUR in S3 + Athena Deepest detail
Actually stop runaway spend Budget action (approval) Applies SCP/IAM or stops instances
Avoid the free-tier cliff Free-tier alert + usage budget 85% + a hard hours threshold

Best practices

Security notes

Cost data is sensitive business information — it reveals architecture, scale, launch timing, and vendor relationships. Treat access to it with least privilege.

Concern Guidance
Who can see cost Grant ce:*, budgets:*, cur:*, aws-portal:ViewBilling only to those who need it; separate from resource admin
Root vs IAM Enabling Cost Explorer and billing preferences may need root/management; do it once, then delegate via IAM
IAM access to Billing The management account must enable IAM user/role access to Billing before member IAM principals can view cost
SNS topic policy Scope the SNS:Publish allow to budgets.amazonaws.com (and cloudwatch.amazonaws.com) — do not open the topic to *
Budget action policies The IAM/SCP a budget action applies is powerful; scope it tightly and prefer approval-required
CUR bucket The S3 bucket holds full billing detail — block public access, encrypt (SSE-S3/KMS), restrict the bucket policy to billingreports.amazonaws.com and your analysts
Cross-account visibility In an org, member accounts see only their own cost unless the payer shares views; don’t leak org-wide cost to a single team
Audit Billing console actions are logged by CloudTrail — see AWS CloudTrail, Config and Audit Compliance

Cost & sizing

The controls themselves are the cheapest insurance in AWS. What each one costs:

Item Cost Notes
Cost Explorer UI Free Unlimited use in the console
Cost Explorer API (ce) $0.01 / request Adds up if you script dashboards; cache results
Cost Explorer hourly + resource-level ~$0.01 / 1,000 usage records / mo Paid opt-in; leave off unless needed
Budgets First 2 free, then $0.02/budget/day ~$0.60/mo per extra budget
CloudWatch billing alarm Free Within the 10-alarm free tier
Cost Anomaly Detection Free Monitors and alerts cost nothing
CUR S3 storage + Athena scan Parquet + partitioning keeps scans cheap
SNS 1M publishes/mo free Email deliveries within free tier for small volumes
Free-tier alerts Free Just email

In INR terms, a complete guardrail set — Cost Explorer, two budgets, a billing alarm, anomaly detection, free-tier alerts — costs effectively ₹0/month. Even a heavy setup (a dozen budgets, a scripted CE dashboard, a CUR feeding QuickSight) is a few hundred rupees a month — trivially less than the first surprise bill it prevents. The right-sizing rule: spend zero on the guardrails, spend your effort on the structural fixes (delete NAT/EIP/EBS waste, right-size RDS/EC2, buy Savings Plans once you have a steady baseline).

Interview & exam questions

1. Why must a CloudWatch billing alarm be created in us-east-1? (CLF-C02, SOA-C02) — Because AWS publishes the AWS/Billing EstimatedCharges metric only to CloudWatch in us-east-1; billing is a global service homed there. An alarm in any other Region sits in INSUFFICIENT_DATA because the metric does not exist locally. You also must enable “Receive Billing Alerts” first.

2. What is the difference between an actual and a forecasted budget alert? (CLF-C02) — An actual alert fires when real, already-incurred spend crosses the threshold; a forecasted alert fires when the projected month-end spend crosses it. Actual never false-alarms but only warns after the money is spent; forecasted warns early but needs history and is noisy on new accounts.

3. Name the three kinds of AWS Free Tier. (CLF-C02) — 12-month free (from sign-up, e.g. 750 EC2 hours/mo, expires at month 13), always-free (forever, monthly, e.g. 1M Lambda requests, 25 GB DynamoDB), and short-term trials (fixed days from first use, e.g. 30-day GuardDuty). Newer accounts may instead have a credit-based free plan.

4. Cost Explorer shows nothing right after you enable it. Why? (CLF-C02) — Cost Explorer is a one-time opt-in that back-fills roughly 24 hours (and up to ~14 months of history over the following day). Empty immediately after enable is expected; return in ~24 hours.

5. Where do NAT gateway and data-transfer costs appear in Cost Explorer? (SOA-C02) — Group by Usage type (not just Service). NAT lands under NatGateway-Hours/NatGateway-Bytes (filed under “EC2-Other”), and transfer under DataTransfer-Out-Bytes/DataTransfer-Regional-Bytes. The resource console does not show these meters.

6. How does the Free Tier behave in an AWS Organization? (CLF-C02) — There is one Free Tier for the whole organization, aggregated at the payer, not one per member account. Ten accounts share 750 free EC2 hours total, not 7,500.

7. What is the closest thing AWS has to a hard spending cap? (CLF-C02, SOA-C02) — A budget action: when a threshold is crossed, it can automatically (or with approval) apply a restrictive IAM policy or SCP, or stop tagged EC2/RDS instances. It still fires on the delayed budget evaluation, so it is not truly instantaneous.

8. When would you use the CUR over Cost Explorer? (SOA-C02) — When you need per-resource, hourly line items, unlimited history, or SQL/BI analysis (chargeback, deep audits). Cost Explorer is capped at ~14 months and coarser resource detail; the CUR delivers full detail to your S3 bucket for Athena/QuickSight.

9. What does Cost Anomaly Detection add over Budgets? (CLF-C02) — It uses ML to learn your normal spend and alerts on deviations without you setting a threshold, catching spikes you never anticipated. Budgets only fire on thresholds you defined in advance.

10. What are unblended vs amortized cost? (SAA-C03) — Unblended is the actual rate charged on each line at the time of use (matches the invoice). Amortized spreads upfront RI/Savings Plans fees across the commitment term, giving a truer per-hour run-rate when you have commitments.

11. Your budget crossed its threshold but no email arrived. First two things to check? (SOA-C02) — (a) Is the SNS email subscription confirmed (not PendingConfirmation)? (b) Does the SNS topic policy allow budgets.amazonaws.com to SNS:Publish? Both are silent failures.

12. What enables the EstimatedCharges metric in the first place? (CLF-C02) — The Receive Billing Alerts preference in Billing preferences → Alert preferences. Until it is on, no AWS/Billing metric is published, even in us-east-1.

Quick check

  1. A teammate created a CloudWatch billing alarm in ap-south-1 and it never fires. What are the two things wrong, and where must the alarm live?
  2. You enable a brand-new account’s budget with a FORECASTED 100% alert and it fires on day two. Is this a bug? What should you have used instead?
  3. Cost Explorer’s “group by Service” shows a big EC2-Other bucket. What single change reveals whether it’s NAT, EBS, or data transfer?
  4. Your $10 budget crossed 80% but no email came, even though the budget shows the alert as triggered. Name the two silent failure points.
  5. In an org with eight member accounts, how many free EC2 hours per month do you actually get across the org, and why does that surprise people?

Answers

  1. Two things: (a) the AWS/Billing EstimatedCharges metric is published only in us-east-1, so an alarm elsewhere has no metric and sits in INSUFFICIENT_DATA; (b) the Receive Billing Alerts preference may not be enabled. The alarm must live in us-east-1.
  2. Not a bug. A new account has no history, so the forecast is unreliable and can project an absurd month-end. Use ACTUAL thresholds (50/80/100%) first and add FORECASTED once a few weeks of steady history exist.
  3. Change group-by from Service to Usage type. “EC2-Other” splits into NatGateway-Hours/-Bytes, EBS:VolumeUsage, DataTransfer-*, etc., pinpointing the meter.
  4. (a) The SNS email subscription is unconfirmed (PendingConfirmation — the recipient never clicked the link); (b) the SNS topic policy doesn’t allow budgets.amazonaws.com to SNS:Publish. Both fail silently.
  5. 750 total, not 6,000. The Free Tier is one allowance for the whole organization, aggregated at the payer account — not one per member account. People assume it multiplies per account.

Glossary

Term Definition
Free Tier Time-limited (12-month), always-free, or short-trial service allowances for new/all accounts
Free-tier usage alert Email when you’re forecast to exceed ~85% of a free-tier limit
Cost Explorer Interactive UI + ce API to visualise and forecast cost by many dimensions
AWS Budgets Threshold-based alerts on cost/usage/RI/SP, optionally with automated actions
Budget action Automation a budget triggers on breach: apply IAM/SCP policy or stop EC2/RDS
Actual alert Fires on already-incurred spend crossing a threshold
Forecasted alert Fires on projected month-end spend crossing a threshold
EstimatedCharges Cumulative month-to-date cost metric in AWS/Billing, us-east-1 only
CloudWatch billing alarm A CloudWatch alarm on EstimatedCharges
Cost Anomaly Detection ML-based detection of spend deviating from a learned baseline
Monitor A CAD object defining what to watch (all services, an account, a tag, a category)
CUR Cost and Usage Report — hourly line-item export to your S3 bucket
Consolidated billing One payer account billed for all member accounts in an Organization
Blended rate Averaged per-unit rate across an org’s accounts
Amortized cost Upfront RI/SP fees spread across the commitment term
Cost allocation tag A tag key activated so Cost Explorer/CUR can break cost down by it
NAT gateway Managed egress device billed per-hour per-AZ plus per-GB — a top surprise-bill culprit
Cost category Rules that group raw cost into business buckets (product/team)

Next steps

You can now build the four guardrails and read where the money goes. Extend outward:

AWSCost ExplorerAWS BudgetsFree TierBillingCost Anomaly DetectionCloudWatchFinOps
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading