The goal
Nobody should learn about an AWS overspend from the invoice. In about ten minutes you can get an email when spending reaches 80% of a limit you set, and another when AWS forecasts you'll go past it.
Warning
A basic budget only alerts you. It doesn't stop any resources, so the spending carries on until someone acts.
Step 1: check your access
Budgets and Cost Explorer need billing permissions. In a member account, the management account may also control what you can see.
aws sts get-caller-identity --query Account --output text
aws budgets describe-budgets --account-id 111122223333
| Error | Meaning | Fix |
|---|---|---|
AccessDeniedException on budgets:ViewBudget | Your role lacks permissions | Ask for budgets:ViewBudget and budgets:ModifyBudget |
User not enabled for cost explorer access | Cost Explorer isn't turned on yet | Open Cost Explorer once in the console; data can take up to a day to appear |
| Billing pages show access denied for an IAM user, even with permissions | IAM access to billing is turned off | The root user enables it in Account settings |
Step 2: create the budget
Save this as budget.json:
{
"BudgetName": "monthly-total",
"BudgetLimit": { "Amount": "500", "Unit": "USD" },
"TimeUnit": "MONTHLY",
"BudgetType": "COST"
}
Save the alerts as notifications.json. This defines one alert at 80% of actual spend and one for a forecast above 100%:
[
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [
{ "SubscriptionType": "EMAIL", "Address": "cloud-team@contoso.com" }
]
},
{
"Notification": {
"NotificationType": "FORECASTED",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 100,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [
{ "SubscriptionType": "EMAIL", "Address": "cloud-team@contoso.com" }
]
}
]
Create it:
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
aws budgets create-budget \
--account-id "$ACCOUNT" \
--budget file://budget.json \
--notifications-with-subscribers file://notifications.json
Check that it exists:
aws budgets describe-budgets --account-id "$ACCOUNT" \
--query "Budgets[].{Name:BudgetName,Limit:BudgetLimit.Amount,Actual:CalculatedSpend.ActualSpend.Amount,Forecast:CalculatedSpend.ForecastedSpend.Amount}" \
--output table
aws budgets describe-notifications-for-budget \
--account-id "$ACCOUNT" --budget-name monthly-total
Use a team mailbox rather than one person's email. People move on and alerts get missed.
Tip
Billing data isn't real time. It's refreshed several times a day, so a sudden spike can take hours to reach your budget. A new account may also need some spending history before forecast alerts work.
Step 3: see what is costing money
When an alert arrives, this answers "which service?". Set the dates to suit (the end date isn't included):
aws ce get-cost-and-usage \
--time-period Start=2026-11-01,End=2026-11-06 \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
--query "ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]" \
--output table
To see when it started, look at it day by day:
aws ce get-cost-and-usage \
--time-period Start=2026-11-01,End=2026-11-06 \
--granularity DAILY \
--metrics UnblendedCost \
--query "ResultsByTime[].[TimePeriod.Start,Total.UnblendedCost.Amount]" \
--output table
To break down one service by usage type, such as finding out whether EC2 spending is compute, EBS or data transfer:
aws ce get-cost-and-usage \
--time-period Start=2026-11-01,End=2026-11-06 \
--granularity MONTHLY \
--metrics UnblendedCost \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["Amazon Elastic Compute Cloud - Compute"]}}' \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--query "ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]" \
--output table
Service names must match exactly. Take them from the first command's output. Each Cost Explorer API request has a small charge, so don't run these in a tight loop.
Common surprises
| Cost you see | Usual reason |
|---|---|
EC2 - Other | EBS volumes, snapshots, NAT gateways and data transfer |
| NAT gateway charges | Private subnets pulling a lot of data through NAT |
| Elastic IP charges | Public IPv4 addresses are charged, including idle ones |
| Costs after stopping an instance | EBS volumes still bill while stopped |
| A region you don't use | Forgotten test resources |
Find idle items with the non-production scheduling guide and the NAT gateway cost guide.
Budgets per team
A budget can be limited to resources with a specific tag. The tag must first be activated as a cost allocation tag in the Billing console, and it only applies to costs from that point on:
{
"BudgetName": "team-payments",
"BudgetLimit": { "Amount": "200", "Unit": "USD" },
"TimeUnit": "MONTHLY",
"BudgetType": "COST",
"CostFilters": { "TagKeyValue": ["user:team$payments"] }
}
Prevent it next time
- Create a budget in every account the day it's created.
- Use both actual and forecast alerts, because a forecast warns you sooner.
- Keep a consistent tagging standard, so spending has an owner.
- When a real spike happens, follow the runaway cost runbook.