Never Get Surprised by an AWS Bill: Setting Up a Free Tier Billing Alarm with CloudWatch
The AWS Free Tier is generous until it isn't — a forgotten EC2 instance, an unexpected data transfer spike, or a misconfigured Lambda can silently push you past the limit. A $5 billing alarm is the cheapest insurance policy you can set up in under 10 minutes.
TL;DR
| Step | Action | Key Detail |
|---|---|---|
| 1 | Enable Billing Alerts | Must be done in us-east-1 (N. Virginia) — billing metrics only exist there |
| 2 | Create SNS Topic | Acts as the notification dispatcher; email subscription requires manual confirmation |
| 3 | Create CloudWatch Alarm | Metric: EstimatedCharges, Namespace: AWS/Billing, Threshold: $5 |
| 4 | Confirm Email | SNS sends a confirmation link — alarm stays in INSUFFICIENT_DATA until confirmed |
Core Fix: Billing metrics are only published to CloudWatch in us-east-1. All CLI commands below must target that region.
Why This Architecture Works: Data Flow Analysis
AWS billing data is aggregated globally and pushed as a CloudWatch metric (EstimatedCharges) into the AWS/Billing namespace — but exclusively in the us-east-1 region. This is a hard platform constraint, not a configuration choice. CloudWatch evaluates the metric against your defined threshold and, on breach, publishes a state-change event to an SNS topic. SNS then fans out that notification to all confirmed subscribers — in this case, your email address via the SMTP protocol.
Analogy: Think of CloudWatch as a security guard watching a single gauge on a control panel. SNS is the PA system the guard uses to broadcast an alert. The guard can only watch gauges in one specific room (us-east-1), regardless of where the actual activity is happening across the building.
Architecture Diagram
Step-by-Step Implementation (CLI First)
Phase 1: Enable Billing Alerts (One-Time Account Setup)
Billing metrics are disabled by default. This must be enabled from the root account or an account with billing console access.
aws ce put-anomaly-monitor --anomaly-monitor ... # Not needed here
# Enable via Billing Preferences (console-only action, or use the following):
aws account put-alternate-contact ... # N/A
# The correct approach: enable via Billing Console preferences
# OR use this CLI workaround to verify it's already enabled:
aws cloudwatch list-metrics \
--namespace AWS/Billing \
--region us-east-1
Note: Enabling billing alerts requires navigating to Billing Console → Billing Preferences → Receive Billing Alerts. This specific toggle has no public CLI equivalent — it is a one-time manual step. Once enabled, the EstimatedCharges metric becomes available in CloudWatch.
Phase 2: Create the SNS Topic and Email Subscription
# Step 1: Create the SNS topic (must be in us-east-1)
aws sns create-topic \
--name billing-alert \
--region us-east-1
# Output: { "TopicArn": "arn:aws:sns:us-east-1:123456789012:billing-alert" }
# Step 2: Subscribe your email to the topic
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:123456789012:billing-alert \
--protocol email \
--notification-endpoint your-email@example.com \
--region us-east-1
After running the subscribe command, AWS sends a confirmation email. You must click the confirmation link before the alarm can deliver notifications. Until confirmed, the subscription status is PendingConfirmation.
Phase 3: Create the CloudWatch Billing Alarm
aws cloudwatch put-metric-alarm \
--alarm-name "FreeTierBillingAlert" \
--alarm-description "Alert when estimated charges exceed $5" \
--metric-name EstimatedCharges \
--namespace AWS/Billing \
--statistic Maximum \
--period 86400 \
--evaluation-periods 1 \
--threshold 5 \
--comparison-operator GreaterThanOrEqualToThreshold \
--dimensions Name=Currency,Value=USD \
--alarm-actions arn:aws:sns:us-east-1:123456789012:billing-alert \
--treat-missing-data notBreaching \
--region us-east-1
Parameter Breakdown
| Parameter | Value | Reason |
|---|---|---|
--statistic | Maximum | Billing metrics use Maximum, not Average — reflects the highest charge in the period |
--period | 86400 | 86400 seconds = 24 hours; billing data updates every ~6 hours but daily granularity is sufficient |
--treat-missing-data | notBreaching | Prevents false alarms when no billing data has been published yet (new accounts) |
--dimensions | Currency=USD | Required dimension to scope the metric to total USD charges |
Phase 4: Verify Alarm State
aws cloudwatch describe-alarms \
--alarm-names "FreeTierBillingAlert" \
--region us-east-1 \
--query 'MetricAlarms[0].{State:StateValue,Threshold:Threshold,SNS:AlarmActions}'
Expected output after email confirmation and metric data arrival:
{
"State": "OK",
"Threshold": 5.0,
"SNS": ["arn:aws:sns:us-east-1:123456789012:billing-alert"]
}
IAM: Minimum Required Permissions
Apply least privilege. The identity creating this alarm needs only the following actions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BillingAlarmSetup",
"Effect": "Allow",
"Action": [
"cloudwatch:PutMetricAlarm",
"cloudwatch:DescribeAlarms",
"cloudwatch:ListMetrics",
"sns:CreateTopic",
"sns:Subscribe",
"sns:GetTopicAttributes"
],
"Resource": "*"
}
]
}
Critical: Enabling billing alerts in the Billing Console requires the aws-portal:ModifyBilling permission (or root access). This is separate from CloudWatch/SNS permissions and is often overlooked.
Cost Impact of This Solution
- CloudWatch Alarm: The first 10 alarms per month are free under the AWS Free Tier. This single alarm costs $0.10/month outside the free tier — negligible.
- SNS Email Notifications: Email notifications via SNS are free (no charge for email protocol deliveries).
- Net cost: Effectively $0 within free tier, $0.10/month beyond it. This alarm pays for itself by preventing a single accidental charge.
Common Pitfalls
- Wrong region: Running
put-metric-alarminus-west-2or any region other thanus-east-1will create the alarm but it will never trigger —EstimatedChargesdata is only published tous-east-1. - Unconfirmed SNS subscription: The alarm will fire and publish to SNS, but no email is delivered until the subscription endpoint is confirmed.
- Billing alerts not enabled: If the Billing Preferences toggle is off,
list-metrics --namespace AWS/Billingreturns empty, and the alarm stays inINSUFFICIENT_DATAindefinitely. - $5 is not a hard limit: This alarm is a notification, not a spending cap. AWS does not automatically stop services when the threshold is crossed.
Glossary
- EstimatedCharges: A CloudWatch metric in the
AWS/Billingnamespace that reflects the estimated total USD cost accrued in the current billing period, updated approximately every 6 hours. - SNS Topic: A Simple Notification Service logical channel that receives published messages and fans them out to all confirmed subscribers (email, SMS, Lambda, SQS, etc.).
- CloudWatch Alarm: A stateful monitor that evaluates a metric against a threshold over a defined period and transitions between
OK,ALARM, andINSUFFICIENT_DATAstates. - INSUFFICIENT_DATA State: The initial alarm state when CloudWatch has not yet received enough metric data points to evaluate the threshold condition.
- Least Privilege: An IAM security principle where an identity is granted only the minimum permissions required to perform its specific task — nothing more.
Comments
Post a Comment