5 Costly AWS Mistakes I Made in My First Cloud Project (and How to Avoid Them)
A surprise AWS bill is every cloud builder's worst nightmare. Here are 5 silent billing traps—from zombie NAT gateways to runaway CloudWatch logs—and the exact fixes to save you money.
The scariest moment for any cloud developer isn’t a failed deployment or a 3:00 AM production crash.It’s opening the AWS Billing Console at the end of the month.When I started building on AWS, I assumed that as long as I stayed within the Free Tier and shut off my EC2 instances, my costs would be zero. Then my monthly invoice arrived: a surprise charge for resources I didn't even realize were running.After spending hours auditing architectures and setting up proper cost guardrails, I realized these billing surprises follow repeatable patterns. Here are the 5 cost traps that caught me off guard, along with the exact code snippets and architectural fixes to stop them before they hit your wallet.1. The "Zombie" NAT GatewayThe Problem:When designing a standard VPC with private subnets, tutorials instinctively tell you to spin up a NAT Gateway so private resources can reach the internet.A single NAT Gateway costs approximately $0.045 per hour (~$32.40/month) simply sitting idle—plus data processing fees ($0.045/GB). If your infrastructure deploys multi-AZ redundancy for dev/test environments, two NAT gateways run you ~$65/month before your code handles a single real user.+-------------------------------------------------------+
| Private Subnet (Lambda / EC2) |
| | |
| | (Requests to s3.amazonaws.com) |
| v |
| [ NAT Gateway ] ---> Charged per hour + per GB! |
| | |
| v |
| Internet ---> Amazon S3 |
+-------------------------------------------------------+
[Upload Image Suggestion 1]Insert an architecture diagram showing traffic routing through a NAT Gateway vs. a VPC Gateway Endpoint.The Fix:Use VPC Endpoints for AWS Services: If your private instances only need to reach Amazon S3 or DynamoDB, set up a VPC Gateway Endpoint. They are 100% free and route traffic internally through AWS’s private network instead of traveling through your NAT Gateway.Tear Down Dev VPCs: If you only need a NAT Gateway during working hours for testing, write an AWS EventBridge rule paired with Lambda to delete it every evening and recreate it via IaC (Terraform/CDK) in the morning.2. CloudWatch Logs Set to "Never Expire"The Problem:By default, when you create a Lambda function or an ECS task, AWS CloudWatch automatically provisions a Log Group with a retention policy of "Never Expire".In development, verbose debug logging feels harmless. But over months of continuous integration runs and load tests, raw log files accumulate into gigabytes of cold data. CloudWatch charges $0.03 per GB for data stored per month. Over dozens of microservices, old logs turn into a quiet, permanent tax on your account.The Fix:Enforce explicit retention periods in your Infrastructure as Code. In AWS CDK (TypeScript), configure your log retention explicitly:TypeScriptimport * as logs from 'aws-cdk-lib/aws-logs';
| Private Subnet (Lambda / EC2) |
| | |
| | (Requests to s3.amazonaws.com) |
| v |
| [ NAT Gateway ] ---> Charged per hour + per GB! |
| | |
| v |
| Internet ---> Amazon S3 |
+-------------------------------------------------------+
[Upload Image Suggestion 1]Insert an architecture diagram showing traffic routing through a NAT Gateway vs. a VPC Gateway Endpoint.The Fix:Use VPC Endpoints for AWS Services: If your private instances only need to reach Amazon S3 or DynamoDB, set up a VPC Gateway Endpoint. They are 100% free and route traffic internally through AWS’s private network instead of traveling through your NAT Gateway.Tear Down Dev VPCs: If you only need a NAT Gateway during working hours for testing, write an AWS EventBridge rule paired with Lambda to delete it every evening and recreate it via IaC (Terraform/CDK) in the morning.2. CloudWatch Logs Set to "Never Expire"The Problem:By default, when you create a Lambda function or an ECS task, AWS CloudWatch automatically provisions a Log Group with a retention policy of "Never Expire".In development, verbose debug logging feels harmless. But over months of continuous integration runs and load tests, raw log files accumulate into gigabytes of cold data. CloudWatch charges $0.03 per GB for data stored per month. Over dozens of microservices, old logs turn into a quiet, permanent tax on your account.The Fix:Enforce explicit retention periods in your Infrastructure as Code. In AWS CDK (TypeScript), configure your log retention explicitly:TypeScriptimport * as logs from 'aws-cdk-lib/aws-logs';
const buildLogGroup = new logs.LogGroup(this, 'AppServiceLogs', {
logGroupName: '/aws/lambda/order-service',
retention: logs.RetentionDays.ONE_WEEK, // Automatically purges logs after 7 days
});
For existing log groups, run a quick one-line AWS CLI script to set retention across all active groups:Bashaws logs describe-log-groups --query 'logGroups[*].logGroupName' --output text |
xargs -n 1 -I {} aws logs put-retention-policy --log-group-name "{}" --retention-in-days 14
3. Abandoned S3 Multipart Uploads & Hidden VersionsThe Problem:If your application uploads large files (like images, archives, or videos) to Amazon S3, AWS automatically utilizes Multipart Uploads. If an upload fails midway or the user closes their browser, S3 keeps those uploaded chunks indefinitely unless instructed otherwise.These incomplete chunks are invisible in the standard S3 console UI, but AWS still bills you for the underlying storage.[Upload Image Suggestion 2]Insert a screenshot of the Amazon S3 Console showing the "Lifecycle rules" configuration screen with the multipart upload checkbox enabled.The Fix:Add an automated S3 Lifecycle rule to prune aborted uploads after 7 days.In an AWS CloudFormation / SAM template:YAMLResources:
AssetsBucket:
Type: AWS::S3::Bucket
Properties:
LifecycleConfiguration:
Rules:
- Id: CleanUpIncompleteUploads
Status: Enabled
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
4. Orphaned EBS Volumes and Old SnapshotsThe Problem:Terminating an EC2 instance does not always terminate its attached Elastic Block Store (EBS) volume. If the volume's DeleteOnTermination flag was unchecked during creation, the EC2 instance disappears—but the provisioned GP3 or IO2 storage continues running at full price.Similarly, automated daily snapshot scripts can silently accumulate hundreds of forgotten snapshots over time.The Fix:Run AWS Cost Explorer and group by Usage Type, filtering for EBS:VolumeUsage.Navigate to EC2 > Elastic Block Store > Volumes, click the filter bar, and select State: Available. Any volume listed here is currently unattached to an instance and burning money. Back it up to S3 if needed, then delete it.5. Flying Blind: Missing the $0.01 Billing AlarmThe Problem:Most developers only configure a billing alert for when costs reach $50 or $100. By the time that notification arrives in your inbox, an infinite recursive Lambda loop or an open endpoint may have already generated a massive bill.[Upload Image Suggestion 3]Insert a screenshot of an AWS Budgets dashboard showing an alert threshold configured at $1.00 or $5.00 with email notifications.The Fix:Create a Zero-Spend / Low-Spend Budget immediately after opening an AWS account.Open the AWS Budgets Console.Click Create Budget and choose the Cost Budget template.Set your target budget to $1.00 / month.Configure notifications to alert your personal email and SMS at 80% ($0.80) and 100% ($1.00) of the budget limit.If an accidental resource starts running, you'll know on day one instead of day thirty.Quick Architecture ChecklistResourceSilent TrapProactive FixVPC / NetworkingIdle NAT GatewaysUse free S3/DynamoDB Gateway EndpointsCloudWatchLogs set to "Never Expire"Enforce 7- or 14-day retention in IaCAmazon S3Aborted Multipart UploadsEnable an S3 Lifecycle Rule to abort in 7 daysAmazon EC2Unattached EBS volumesAudit and prune volumes in available stateAccount LevelChecking bills manuallySet up a $1.00 AWS Budget alertWhat's Your Cloud War Story?Every single cloud engineer has made a configuration mistake that taught them an unforgettable lesson.What was the most surprising or painful AWS billing trap you ran into when you were getting started? How did you end up fixing it?Drop your best war story in the comments below—let’s help each other build resilient, cost-effective architectures!
logGroupName: '/aws/lambda/order-service',
retention: logs.RetentionDays.ONE_WEEK, // Automatically purges logs after 7 days
});
For existing log groups, run a quick one-line AWS CLI script to set retention across all active groups:Bashaws logs describe-log-groups --query 'logGroups[*].logGroupName' --output text |
xargs -n 1 -I {} aws logs put-retention-policy --log-group-name "{}" --retention-in-days 14
3. Abandoned S3 Multipart Uploads & Hidden VersionsThe Problem:If your application uploads large files (like images, archives, or videos) to Amazon S3, AWS automatically utilizes Multipart Uploads. If an upload fails midway or the user closes their browser, S3 keeps those uploaded chunks indefinitely unless instructed otherwise.These incomplete chunks are invisible in the standard S3 console UI, but AWS still bills you for the underlying storage.[Upload Image Suggestion 2]Insert a screenshot of the Amazon S3 Console showing the "Lifecycle rules" configuration screen with the multipart upload checkbox enabled.The Fix:Add an automated S3 Lifecycle rule to prune aborted uploads after 7 days.In an AWS CloudFormation / SAM template:YAMLResources:
AssetsBucket:
Type: AWS::S3::Bucket
Properties:
LifecycleConfiguration:
Rules:
- Id: CleanUpIncompleteUploads
Status: Enabled
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
4. Orphaned EBS Volumes and Old SnapshotsThe Problem:Terminating an EC2 instance does not always terminate its attached Elastic Block Store (EBS) volume. If the volume's DeleteOnTermination flag was unchecked during creation, the EC2 instance disappears—but the provisioned GP3 or IO2 storage continues running at full price.Similarly, automated daily snapshot scripts can silently accumulate hundreds of forgotten snapshots over time.The Fix:Run AWS Cost Explorer and group by Usage Type, filtering for EBS:VolumeUsage.Navigate to EC2 > Elastic Block Store > Volumes, click the filter bar, and select State: Available. Any volume listed here is currently unattached to an instance and burning money. Back it up to S3 if needed, then delete it.5. Flying Blind: Missing the $0.01 Billing AlarmThe Problem:Most developers only configure a billing alert for when costs reach $50 or $100. By the time that notification arrives in your inbox, an infinite recursive Lambda loop or an open endpoint may have already generated a massive bill.[Upload Image Suggestion 3]Insert a screenshot of an AWS Budgets dashboard showing an alert threshold configured at $1.00 or $5.00 with email notifications.The Fix:Create a Zero-Spend / Low-Spend Budget immediately after opening an AWS account.Open the AWS Budgets Console.Click Create Budget and choose the Cost Budget template.Set your target budget to $1.00 / month.Configure notifications to alert your personal email and SMS at 80% ($0.80) and 100% ($1.00) of the budget limit.If an accidental resource starts running, you'll know on day one instead of day thirty.Quick Architecture ChecklistResourceSilent TrapProactive FixVPC / NetworkingIdle NAT GatewaysUse free S3/DynamoDB Gateway EndpointsCloudWatchLogs set to "Never Expire"Enforce 7- or 14-day retention in IaCAmazon S3Aborted Multipart UploadsEnable an S3 Lifecycle Rule to abort in 7 daysAmazon EC2Unattached EBS volumesAudit and prune volumes in available stateAccount LevelChecking bills manuallySet up a $1.00 AWS Budget alertWhat's Your Cloud War Story?Every single cloud engineer has made a configuration mistake that taught them an unforgettable lesson.What was the most surprising or painful AWS billing trap you ran into when you were getting started? How did you end up fixing it?Drop your best war story in the comments below—let’s help each other build resilient, cost-effective architectures!
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article