Automating AWS Cost Monitoring with CloudWatch and Lambda
What you will build: a cost monitoring loop that compares today against yesterday, breaks spend down by service, and pages you on an anomaly, before the monthly invoice explains it for you.
Checking the billing console by hand is a losing game. A misconfigured job that starts on a Friday evening has all weekend to run, and you find out on the first of the month. The fix is a small automated loop that watches the numbers when nobody is looking.
Architecture overview
System components
- CloudWatch billing metrics for near real-time cost data
- Lambda for processing and comparison
- SNS for notification fan-out
- DynamoDB for historical tracking
- EventBridge for scheduled execution
Step 1: enable billing metrics
Billing metrics publish to us-east-1 only, regardless of where your workloads run. Set the alarm there.
AWS CLI
aws cloudwatch put-metric-alarm \ --alarm-name "Daily-Spend-Alarm" \ --alarm-description "Alert when daily spend exceeds threshold" \ --metric-name EstimatedCharges \ --namespace AWS/Billing \ --statistic Maximum \ --period 86400 \ --threshold 100 \ --comparison-operator GreaterThanThreshold
Step 2: the monitoring Lambda
A daily comparison catches the majority of cost incidents. Anything that jumps more than 20% day over day is worth a look.
Lambda handler (Python)
import boto3
import json
from datetime import datetime, timedelta
def lambda_handler(event, context):
cloudwatch = boto3.client('cloudwatch')
sns = boto3.client('sns')
# Get current and previous day costs
current_cost = get_daily_cost(cloudwatch, 0)
previous_cost = get_daily_cost(cloudwatch, 1)
# Calculate percentage change
if previous_cost > 0:
change_percent = ((current_cost - previous_cost) / previous_cost) * 100
else:
change_percent = 0
# Alert if increase > 20%
if change_percent > 20:
send_alert(sns, current_cost, change_percent)
return {
'statusCode': 200,
'body': json.dumps(f'Cost monitoring completed. Change: {change_percent:.2f}%')
}
def get_daily_cost(cloudwatch, days_ago):
end_time = datetime.utcnow() - timedelta(days=days_ago)
start_time = end_time - timedelta(days=1)
response = cloudwatch.get_metric_statistics(
Namespace='AWS/Billing',
MetricName='EstimatedCharges',
Dimensions=[{'Name': 'Currency', 'Value': 'USD'}],
StartTime=start_time,
EndTime=end_time,
Period=86400,
Statistics=['Maximum']
)
return response['Datapoints'][0]['Maximum'] if response['Datapoints'] else 0Going further
Break the number down by service
Knowing the bill went up 30% is not actionable. Knowing that NAT Gateway data processing went up 30% is. Group the Cost Explorer query by service so the alert arrives with a suspect already named.
def get_service_costs(ce_client):
response = ce_client.get_cost_and_usage(
TimePeriod={
'Start': (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d'),
'End': datetime.now().strftime('%Y-%m-%d')
},
Granularity='DAILY',
Metrics=['UnblendedCost'],
GroupBy=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}]
)
services = {}
for result in response['ResultsByTime']:
for group in result['Groups']:
service = group['Keys'][0]
cost = float(group['Metrics']['UnblendedCost']['Amount'])
services[service] = cost
return servicesLet AWS do the anomaly detection
Before you write your own forecasting, turn on AWS Cost Anomaly Detection. It builds a baseline per service and flags deviations against it, which handles the seasonal patterns that a naive day over day comparison keeps false-alarming on. Use your Lambda for the checks it does not cover, not as a replacement.
Pro tip
Set separate thresholds per team or project using cost allocation tags, and route each alert to the channel that owns the spend. A single alert to a shared inbox gets muted within a month.
Monitoring tells you when spend jumps. Our AWS cost audit finds what is driving it, at a fixed price.