awscost

How to avoid a surprise AWS bill on a new project

Where surprise AWS bills come from on small projects, and the six habits that keep an empty project at $0: idle-free services, one account each, budget alerts.

By Pierre Sauvignon · · 7 min read

The TigerSoup tiger relaxing in a deck chair, holding a piggy bank with a small cloud on it, a calm alarm bell floating nearby

A surprise AWS bill almost never comes from a project that got popular. It comes from something you forgot was running. The good news is that the defences are cheap, and most of them take minutes.

This post walks through where surprise bills actually come from on small projects, then the habits that stop them, in order of effort. If you follow all six, an empty project stays at $0 and a busy one stays predictable.

Where surprise AWS bills come from

Nearly every surprise AWS bill on a small project traces back to one of two things: a resource that bills by the hour whether or not you use it, or a leaked access key. Traffic spikes are a distant third.

The hourly offenders are easy to list because they show up in the same forum threads year after year:

The second category is worse. If an access key leaks, through a public repo, a pasted log, or a compromised laptop, someone else runs expensive instances on your account, often for crypto mining. Those bills arrive in the thousands, not the tens.

The $0-idle rule

Pick services that cost nothing when nobody is using them, and an unused project can’t cost you anything. This is the single most effective habit, and it’s a design choice you make once at the start.

AWS has more than 30 services with an always-free monthly allowance that doesn’t expire. Lambda includes one million requests a month. DynamoDB in on-demand mode bills per request. S3 bills per gigabyte stored and per request. CloudFront has a permanent free allowance. A small web app built on these sits at $0 until real people turn up, and then grows in proportion to them. See the AWS Free Tier page for the current list.

Here’s the trade-off table for the usual suspects:

ThingIdle costSafer alternative
EC2 instanceHourly, alwaysLambda, or stop the instance when done
RDS databaseHourly, alwaysDynamoDB on-demand, or Aurora Serverless v2 scaled to zero
NAT gatewayHourly plus per GBSkip private subnets for small projects, or use VPC endpoints
Application Load BalancerHourly, alwaysAPI Gateway or a Lambda function URL
Provisioned DynamoDBFixed capacityOn-demand mode
OpenSearch domainHourly, alwaysOnly create it when you have search traffic

The point isn’t that EC2 or RDS are bad. They’re the right call for some workloads. The point is that for a project at the “idea” stage, a $0-idle stack means the question “is this costing me money?” always has the same answer.

One account per project makes bills readable

Give each project its own AWS account and every bill has one owner. When five projects share an account, a $40 charge is a mystery that needs tags and a cost explorer session to solve. When each project has its own account, a $40 charge belongs to exactly one thing.

This also limits the damage. If a test resource in one project runs away, it can’t touch the budget of another, and shutting the whole project down is one account closure rather than a hunt through shared resources. There’s a longer argument for this in why every project should have its own AWS account.

A per-project account is free. AWS doesn’t charge for accounts, and an AWS Organization lets you create them under one bill. One caveat worth knowing: AWS moves accounts that use Organizations onto the Paid plan, so the sign-up credits don’t apply the way they do on a standalone account. The always-free allowances still do. There’s more detail in the AWS Free Tier, explained.

Set a budget alert (two minutes)

Create a budget in the AWS Billing console with an email alert, and AWS warns you before spending passes a number you pick. This is the cheapest insurance in cloud computing, and most people skip it.

  1. Open the AWS Billing and Cost Management console and choose Budgets.
  2. Choose Create budget, then the Zero spend budget template if you want to know the moment anything costs money, or a Monthly cost budget with a dollar amount.
  3. Enter the email address that should get the alert.
  4. Create it.

That’s the whole process. The zero-spend template is ideal for a project that should sit at $0: the first cent triggers an email. For a project with known costs, set the monthly amount a little above what you expect.

Two things to add once you’ve done that:

If you have one account per project, do this in every account. It’s two minutes each.

Keys are the biggest risk

Long-lived access keys are the fastest route to a bill in the thousands, so the safest key is one that doesn’t exist. Every surprise-bill horror story that involves “someone else’s instances” starts with a key that leaked and didn’t expire.

For your own machine, use the AWS CLI with SSO or short-lived session credentials rather than a permanent key pair in ~/.aws/credentials. For deploys, don’t paste keys into GitHub secrets at all. GitHub Actions can request short-lived credentials from AWS directly using OpenID Connect, and the trust policy can be locked to one repository and one branch. Those credentials expire in about an hour, so there’s nothing to leak. The setup is covered step by step in keyless deploys with GitHub Actions and OIDC.

This also matters more now that AI coding agents run commands on your behalf. An agent with a permanent admin key on the dev machine has every tool it needs to cause an expensive mistake. If the only credentials around are short-lived and scoped, the worst case shrinks a lot.

The teardown habit

Deploy everything as one stack, and tearing it down is one command. If your infrastructure lives in CloudFormation, CDK, SAM, Terraform, or anything with a destroy verb, you can delete an experiment completely rather than hoping you remembered every resource.

The habit that makes this work: never create resources by hand in the console for anything that isn’t a one-off look. Console-created resources are the ones that outlive the experiment, because nothing tracks them. Stack-created resources disappear together.

For a quick experiment, name the stack after the date or the branch, try the thing, and run cdk destroy or aws cloudformation delete-stack when you’re done. If you’re using one account per project, you can also close the entire account when a project ends, which removes any chance of a straggler.

What this looks like in practice

A project set up this way has a predictable shape:

None of this is advanced. It’s the setup an experienced team would do on day one, which is exactly why it gets skipped by people in a hurry. If you want the full day-one list, including GitHub and environment separation, it’s in the new project setup checklist.

This is also the setup TigerSoup builds for each new project: a separate AWS account per project, deploys that use short-lived credentials instead of keys, and nothing that runs around the clock. It’s a free Mac app, and nothing it creates costs anything until you run something. You’ll still want to add the budget alert yourself. It’s two minutes, and it’s the one thing nobody regrets.

Frequently asked questions

Does an empty AWS account cost anything?

No. AWS has no monthly fee. An account with nothing running in it costs $0, and so do accounts, IAM roles, and deploy connections on their own.

What causes most surprise AWS bills on small projects?

Something left running: an idle database, a NAT gateway, a forgotten test server, or provisioned capacity nobody is using. Leaked access keys are the other big one.

How do I get warned before I spend money on AWS?

Create a budget in the AWS Billing console with an email alert at a dollar amount you choose. It takes about two minutes and emails you before spending passes that number.