AWS bills grow quietly. A dev environment EC2 left running over a holiday weekend, an S3 bucket with no lifecycle policy accumulating five years of logs, an RDS instance sized for peak traffic that never arrived — these add up. Remote teams with multiple developers provisioning infrastructure independently need guardrails.
This guide covers practical cost control for remote AWS teams: budget alerts, anomaly detection, right-sizing, reserved capacity, and automated cleanup of abandoned resources.
Set Up Budget Alerts First
Before anything else, configure billing alerts so you know when spending deviates.
# Create a monthly budget with email alert at 80% and 100%
aws budgets create-budget \
--account-id $(aws sts get-caller-identity --query Account --output text) \
--budget '{
"BudgetName": "Monthly-Total",
"BudgetLimit": {"Amount": "500", "Unit": "USD"},
"TimeUnit": "MONTHLY",
"BudgetType": "COST"
}' \
--notifications-with-subscribers '[
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": "team@yourcompany.com"}]
},
{
"Notification": {
"NotificationType": "FORECASTED",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 100,
"ThresholdType": "PERCENTAGE"
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": "team@yourcompany.com"}]
}
]'
Consider creating per-project budgets in addition to the overall monthly cap. If your team runs five active projects, give each a budget envelope — this exposes overspend at the project level before it rolls up into the total.
Enable Cost Anomaly Detection
AWS Anomaly Detection uses ML to flag unexpected spending spikes before they become large bills.
# Create a monitor for all AWS services
aws ce create-anomaly-monitor \
--anomaly-monitor '{
"MonitorName": "AllServices",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "SERVICE"
}'
# Get the monitor ARN from the response, then create an alert subscription
aws ce create-anomaly-subscription \
--anomaly-subscription '{
"SubscriptionName": "DailyAnomalyAlert",
"MonitorArnList": ["arn:aws:ce::ACCOUNT_ID:anomalymonitor/MONITOR_ID"],
"Subscribers": [
{"Address": "team@yourcompany.com", "Type": "EMAIL"}
],
"Threshold": 50,
"Frequency": "DAILY"
}'
The Threshold: 50 means you get alerted when an anomaly exceeds $50 above expected spending. Tune this threshold based on your typical bill — a $500/month account should alert at $25, while a $5000/month account might tolerate $150 before alerting.
You can also create service-specific monitors. Separate monitors for EC2 and RDS give finer granularity — a spike in one service doesn’t get masked by normal variance in another.
Tag Every Resource
Tags are the foundation of cost attribution in remote teams. Without them, you cannot tell which project or developer generated a bill.
# Required tags policy — enforce via AWS Organizations SCP or tag policies
# Minimum required tags for all resources:
# - Project: project-name
# - Owner: developer-email
# - Environment: dev|staging|prod
# Apply tags to an existing EC2 instance
aws ec2 create-tags \
--resources i-1234567890abcdef0 \
--tags \
Key=Project,Value=myapp \
Key=Owner,Value=mike@company.com \
Key=Environment,Value=dev
# Find all untagged EC2 instances
aws resourcegroupstaggingapi get-resources \
--resource-type-filters ec2:instance \
--tag-filters Key=Owner,Values=[] \
--query 'ResourceTagMappingList[?Tags==`[]`].ResourceARN' \
--output text
Enforce tagging at the IAM level using AWS Organizations tag policies. A policy can prevent resource creation if mandatory tags are missing — developers quickly learn to tag when the alternative is a permission error. Start with soft enforcement (reports of untagged resources) and move to hard enforcement after a 30-day grace period.
For teams using Terraform or CloudFormation, add required tags as a module-level default rather than asking developers to add them per-resource. This reduces friction and ensures consistency.
Find Idle and Underutilized Resources
# Find EC2 instances with < 5% average CPU over 14 days
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--dimensions Name=InstanceId,Value=i-1234567890abcdef0 \
--start-time $(date -u -d "14 days ago" +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 1209600 \
--statistics Average \
--query 'Datapoints[0].Average'
# Use AWS Compute Optimizer for automated right-sizing recommendations
aws compute-optimizer get-ec2-instance-recommendations \
--query 'instanceRecommendations[?finding==`OVER_PROVISIONED`].[instanceArn,currentInstanceType,recommendationOptions[0].instanceType,recommendationOptions[0].estimatedMonthlySavings.value]' \
--output table
Compute Optimizer looks at 14 days of CloudWatch metrics by default. Opt in to 3-month lookback periods for better recommendations — this is especially important for workloads with weekly or monthly cycles that 14 days would misread as idle.
Beyond EC2, check for orphaned resources your team commonly forgets:
# Find unattached EBS volumes (still incurring storage costs)
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[*].[VolumeId,Size,CreateTime,Tags]' \
--output table
# Find unused Elastic IPs (charged when not attached)
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' \
--output table
# Find Load Balancers with no registered targets
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[*].[LoadBalancerArn,LoadBalancerName,CreatedTime]' \
--output table
Run this audit monthly and post results to a #aws-costs Slack channel. Making idle resource data visible to the whole team creates peer accountability without requiring a dedicated FinOps function.
Stop Dev Instances Outside Business Hours
# Lambda function to stop non-prod instances at 8pm, start at 8am
# Create with this policy attached to a Lambda role
# stop-instances.py
import boto3
def lambda_handler(event, context):
ec2 = boto3.resource('ec2')
# Find all running instances tagged Environment=dev
instances = ec2.instances.filter(
Filters=[
{'Name': 'tag:Environment', 'Values': ['dev', 'staging']},
{'Name': 'instance-state-name', 'Values': ['running']}
]
)
instance_ids = [i.id for i in instances]
if instance_ids:
ec2.instances.filter(InstanceIds=instance_ids).stop()
print(f"Stopped {len(instance_ids)} instances: {instance_ids}")
return {'stopped': instance_ids}
# Schedule with EventBridge (CloudWatch Events)
aws events put-rule \
--name "StopDevInstances" \
--schedule-expression "cron(0 20 * * ? *)" \
--state ENABLED
aws events put-rule \
--name "StartDevInstances" \
--schedule-expression "cron(0 8 * * ? *)" \
--state ENABLED
This alone can cut EC2 costs by 60% for dev environments — they run 12 hours instead of 24.
For remote teams distributed across time zones, use the team’s primary timezone for scheduling and communicate the schedule clearly in your engineering handbook. Developers who need an instance outside hours can tag it with SkipShutdown=true — the Lambda should respect that tag, but auto-remove it after 24 hours to prevent permanent bypass.
S3 Lifecycle Policies
S3 costs accumulate through log archives and backups with no expiry. Set lifecycle rules on every bucket.
# Apply lifecycle policy to a log bucket
aws s3api put-bucket-lifecycle-configuration \
--bucket my-app-logs \
--lifecycle-configuration '{
"Rules": [
{
"ID": "LogArchiveAndDelete",
"Status": "Enabled",
"Filter": {"Prefix": "logs/"},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER_IR"
}
],
"Expiration": {
"Days": 365
},
"NoncurrentVersionExpiration": {
"NoncurrentDays": 30
}
}
]
}'
Logs move from Standard ($0.023/GB) to Standard-IA at 30 days ($0.0125/GB), to Glacier at 90 days ($0.004/GB), and delete at 1 year.
Apply separate policies for different data types. Application logs can expire at 1 year, but audit logs may need 7-year retention for compliance. Terraform module outputs — build artifacts, deployment packages — typically need only 90 days. Define a lifecycle policy template per data category in your infrastructure-as-code repo so every new bucket starts with the right policy by default.
Right-Size RDS Instances
# Check RDS CPU and connection utilization
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name CPUUtilization \
--dimensions Name=DBInstanceIdentifier,Value=mydb \
--start-time $(date -u -d "30 days ago" +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 2592000 \
--statistics Average Maximum \
--output table
# If max CPU < 40% sustained, downsize
# db.t3.medium → db.t3.small: ~$35/mo savings
# db.r5.large → db.t3.medium: ~$110/mo savings
# Modify instance class (applies at next maintenance window)
aws rds modify-db-instance \
--db-instance-identifier mydb \
--db-instance-class db.t3.medium \
--apply-immediately
Look at both CPU and DatabaseConnections metrics together. An instance with 10% average CPU but 90% of max connections isn’t over-provisioned on compute — it’s under-provisioned on connections. Downsizing would cause connection exhaustion under normal load.
RDS Aurora Serverless v2 is worth evaluating for staging and lower-traffic production workloads. It scales from 0.5 ACUs to 128 ACUs in seconds, which eliminates the over-provisioning problem for variable workloads. A staging database that runs $150/month as a fixed instance often costs $20-40/month on Aurora Serverless v2 with realistic usage patterns.
Use Savings Plans for Predictable Workloads
For any EC2 or Fargate workload running 24/7, Savings Plans deliver 40-60% savings over on-demand.
# Check your on-demand spend eligible for Savings Plans
aws ce get-savings-plans-purchase-recommendation \
--savings-plans-type COMPUTE_SP \
--term-in-years ONE_YEAR \
--payment-option NO_UPFRONT \
--lookback-period-in-days THIRTY_DAYS \
--query 'SavingsPlansPurchaseRecommendation.SavingsPlansPurchaseRecommendationDetails[0].[HourlyCommitmentToPurchase,EstimatedSavingsPercentage,EstimatedMonthlySavingsAmount]' \
--output table
Buy Compute Savings Plans (not EC2 instance plans) — they apply across instance families, regions, and operating systems. One-year, no-upfront is the lowest-risk entry.
Purchase at 70-80% of your sustained baseline, not your peak. If your production fleet runs consistently at $300/month but spikes to $400/month twice a year, commit to $210-240/month in Savings Plans. The remaining usage runs on-demand, which is acceptable. Over-committing creates wasted spend if workloads shrink.
Cost Explorer Report by Tag
# Monthly cost by Project tag
aws ce get-cost-and-usage \
--time-period Start=2026-03-01,End=2026-03-31 \
--granularity MONTHLY \
--filter '{"Tags":{"Key":"Environment","Values":["prod"]}}' \
--group-by '[{"Type":"TAG","Key":"Project"}]' \
--metrics BlendedCost \
--query 'ResultsByTime[0].Groups[*].[Keys[0],Metrics.BlendedCost.Amount]' \
--output table
Run this weekly and share it with the team in Slack. Visibility into which projects are spending what changes behavior faster than any policy.
Consider building a simple weekly cost digest using this query in a Lambda function triggered by EventBridge. Format the output as a Slack message with project names, current month spend, and month-over-month delta. Teams that see their infrastructure costs weekly develop cost-aware habits without requiring a dedicated FinOps process.
Building a Cost Review Cadence
Cost control works best when it’s part of engineering culture, not a periodic audit. Structure a lightweight monthly review:
- Pull the tagging compliance report — any untagged resources from the past month
- Review Compute Optimizer recommendations — dismiss ones that don’t apply, act on the ones that do
- Check Savings Plan utilization — if you’re consistently at 100%, you have room to commit more
- Review S3 storage growth by bucket — catch buckets growing unexpectedly before they compound
- Share the month-over-month total with the team
This review takes 30 minutes, can be done async over a shared doc, and prevents the quarterly bill surprise that’s common in remote teams where no one owns infrastructure costs explicitly.
Related Reading
- CI/CD Pipeline for Solo Developers: GitHub Actions
- Prometheus Monitoring Setup for Remote Infrastructure
- Portable Dev Environment with Docker 2026
Built by theluckystrike — More at zovo.one