Problem Summary
An unexpected spike in AWS spend is one of the most stressful infrastructure events because it combines financial urgency with technical ambiguity. The bill has already accrued; every minute of continued diagnosis is costing money. This guide gives you a structured 30-minute investigation path using Cost Explorer, the AWS CLI, and service-specific consoles to isolate the charge source and take immediate corrective action.
Symptoms
- AWS Cost Explorer shows a sharp vertical jump in spend starting at a specific date and time
- Month-to-date charges are already at or above last month's total bill
- A billing alert email arrived from AWS (if you had budget alerts configured)
- One specific AWS service dominates the unexpected increase in the Cost Explorer breakdown
- Usage metrics do not correlate with the cost increase traffic is normal but spend is not
- New line items appear in the bill that were not present in previous months
- Data transfer or NAT Gateway line items are significantly larger than historical norms
- No new production deployments happened, yet costs increased
Business Impact
A doubled AWS bill represents an immediate financial impact. For startups on tight budgets, an unexpected $5,000-$50,000 charge can threaten runway. Beyond the immediate cost, unchecked AWS spend often indicates a systemic architecture problem data being transferred unnecessarily, resources left running from past experiments, or an autoscaling policy that has no upper bound. Fixing the symptom without fixing the root cause means the spike will recur.
Root Cause
The most common root causes in order of frequency: NAT Gateway data processing charges from application-level logging or S3 traffic routed through NAT instead of a VPC endpoint, forgotten EC2 or RDS instances left running from development experiments, S3 cross-region replication or data transfer out to internet costs, unexpected autoscaling event that launched large instance types, CloudWatch log ingestion from verbose application logging, and Elastic IPs that are allocated but not attached to a running instance (charged at $0.005/hr when unattached).
NAT Gateway charges are the single most surprising AWS cost for engineering teams. NAT Gateway costs $0.045 per GB of data processed if your EC2 instances are sending application logs to CloudWatch or pulling Docker images from ECR through a NAT Gateway, this adds up extremely fast.
Diagnosis
Step 1 Use Cost Explorer to Identify the Service and Time
Open AWS Console → Cost Management → Cost Explorer. Set the time range to the last 30 days, group by Service, and identify which service shows the spike. Then drill down by filtering to that service and grouping by Usage Type to find the specific charge category.
aws ce get-cost-and-usage --time-period Start=2026-07-01,End=2026-07-24 --granularity DAILY --metrics BlendedCost --group-by Type=DIMENSION,Key=SERVICE --output tableaws ce get-cost-and-usage --time-period Start=2026-07-20,End=2026-07-24 --granularity HOURLY --metrics BlendedCost --group-by Type=DIMENSION,Key=USAGE_TYPE --filter '{"Dimensions":{"Key":"SERVICE","Values":["Amazon EC2"]}}' --output tableStep 2 Check for Forgotten Running Instances
Scan all regions for running EC2 instances and RDS databases. Engineers frequently launch instances in non-default regions for experiments and forget them. Use the CLI to check every region.
#!/bin/bash
# List all running EC2 instances across every region
for region in $(aws ec2 describe-regions --query 'Regions[*].RegionName' --output text); do
instances=$(aws ec2 describe-instances --region "$region" --filters Name=instance-state-name,Values=running --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,LaunchTime,Tags[?Key==`Name`].Value|[0]]' --output text 2>/dev/null)
if [ -n "$instances" ]; then
echo "=== Region: $region ==="
echo "$instances"
fi
doneaws rds describe-db-instances --query 'DBInstances[*].[DBInstanceIdentifier,DBInstanceClass,DBInstanceStatus,MultiAZ,StorageAllocated]' --output tableStep 3 Investigate NAT Gateway Data Transfer
If NAT Gateway is the top charge, the fix is usually routing S3 and DynamoDB traffic through VPC Gateway Endpoints (free) instead of NAT Gateway. First, measure your NAT Gateway throughput to confirm the scale.
aws cloudwatch get-metric-statistics --namespace AWS/NATGateway --metric-name BytesOutToDestination --dimensions Name=NatGatewayId,Value=<NAT_GW_ID> --start-time 2026-07-20T00:00:00Z --end-time 2026-07-24T00:00:00Z --period 3600 --statistics Sum --output tableCreate a VPC Gateway Endpoint for S3 in your VPC (free, takes 2 minutes). Traffic from EC2 instances to S3 in the same region then bypasses NAT Gateway entirely. For ECR, use VPC Interface Endpoints. Together these often reduce NAT Gateway costs by 60-80%.
Step 4 Find Unattached Elastic IPs and Unused Resources
Elastic IPs cost $0.005/hr when not attached to a running instance. Small individually, but teams accumulate dozens. Also check for unattached EBS volumes, which accrue storage charges indefinitely.
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId,Domain]' --output tableaws ec2 describe-volumes --filters Name=status,Values=available --query 'Volumes[*].[VolumeId,Size,VolumeType,CreateTime]' --output tableStep 5 Audit CloudWatch Log Ingestion and S3 Request Costs
CloudWatch log ingestion costs $0.50/GB. Verbose DEBUG logging in production can ingest hundreds of GB per day. Check log ingestion rates and set log group retention policies.
aws logs describe-log-groups --query 'logGroups[*].[logGroupName,storedBytes,retentionInDays]' --output table | sort -k2 -rn | head -20#!/bin/bash
# Set 14-day retention on all log groups that have no retention policy
aws logs describe-log-groups --query 'logGroups[?retentionInDays==`null`].logGroupName' --output text | tr '\t' '\n' | while read group; do
echo "Setting 14-day retention on: $group"
aws logs put-retention-policy --log-group-name "$group" --retention-in-days 14
doneProduction Failure Scenarios
Scenario 1 Autoscaling Launched GPU Instances
An autoscaling group was configured with p3.2xlarge instances as the scale-out type for a traffic spike. The spike occurred, 20 instances launched, traffic subsided but the scale-in policy had a 2-hour cooldown. At $3.06/hr per instance that is $1,224 for a 20-instance-hour burst. Check autoscaling activity: aws autoscaling describe-scaling-activities --auto-scaling-group-name <name>
Scenario 2 S3 Cross-Region Replication Enabled Accidentally
Someone enabled cross-region replication on a bucket with 10 TB of data. Replication transfer costs $0.02/GB. The initial sync cost $200 and will continue to cost for every new object. Check bucket replication config: aws s3api get-bucket-replication --bucket <bucket-name>
Scenario 3 RDS Reserved Instance Expired
A 1-year RDS Reserved Instance expired last month. The instance silently fell back to on-demand pricing, which is 3-4x more expensive. There is no notification when Reserved Instances expire. Check reservation status in the RDS console under Reserved Instances, or: aws rds describe-reserved-db-instances
Prevention Checklist
- Set up AWS Budgets alerts at 50%, 80%, and 100% of expected monthly spend
- Enable AWS Cost Anomaly Detection it uses ML to detect unusual spending patterns and sends daily/weekly alerts
- Create VPC Gateway Endpoints for S3 and DynamoDB in every VPC to eliminate unnecessary NAT Gateway charges
- Tag all resources with Environment, Team, and Project tags untagged resources are invisible in cost analysis
- Use AWS Trusted Advisor to get a monthly report of idle instances, unattached EIPs, and underutilized resources
- Set a maximum capacity on every autoscaling group never leave max-capacity as unlimited
- Set log retention policies on all CloudWatch log groups 30 days for production, 7 days for dev/staging
- Review Reserved Instance and Savings Plans expiry dates monthly and renew 30 days before expiry
- Run a monthly 'resource cleanup' ritual: delete snapshots older than 90 days, release unattached EIPs, delete available EBS volumes
- Enable S3 Intelligent-Tiering or lifecycle policies on buckets older than 30 days
Resolution Summary
Start with Cost Explorer to identify the service and time of the spike within 5 minutes. Then follow the service-specific diagnosis: for NAT Gateway, add VPC endpoints; for EC2/RDS, terminate forgotten instances; for S3, check for new replication or large transfer-out events; for CloudWatch, cap log ingestion with retention policies. Most bill doublings have a single dominant cause that, once fixed, immediately stops the bleeding.
Lessons Learned
- AWS billing alerts are the single highest-ROI infrastructure investment set them before you need them
- NAT Gateway is the most commonly misunderstood AWS cost; every team should add VPC endpoints on day one
- Reserved Instance and Savings Plans expirations are silent calendar reminders are non-negotiable
- Resource tagging is a prerequisite for cost accountability without tags you cannot assign cost to a team or project
- Autoscaling max-capacity limits prevent runaway costs; treat them as a financial safety circuit breaker
Conclusion
Unexpected AWS bill spikes are almost always preventable in retrospect. The investigation process described here Cost Explorer for service identification, CLI commands for resource enumeration, and service-specific metrics for confirmation can identify the root cause in under 30 minutes. Once you have found and fixed the immediate issue, implement the prevention checklist to build cost visibility into your operational baseline. A bill that doubled overnight is a symptom of missing observability, not just a cost problem.