Here is a sample CDK app with four stacks. Nobody is calling it. No requests, no data, no users.
ap-northeast-1 · 4 stacks · idle ≈ $382/mo
DataStack $196/mo
Aurora/writer ×0.5 ACU $55 Serverless v2 minimum capacity
Search ×1 $41 t3.small.search data nodes
Search ×20 GB gp3 $2.93 EBS storage
Db ×1 $36 db.t4g.micro postgres Multi-AZ
Db ×20 GB gp3 $5.52 storage
...
NetworkStack $69/mo
Vpc/PublicSubnet1/NATGateway ×1 $45 NAT Gateway
Vpc/SsmEndpoint ×2 AZ $20 interface endpoint
Vpc/PublicSubnet1/EIP ×1 $3.65 public IPv4 address
? could not estimate: 1
AppStack ParamInstance InstanceType is Parameter WorkerType
prices: bundled snapshot 2026-10-15 · on-demand list prices, 730 h/mo, USD, free tier ignored
It still costs about $382 a month. That output came from two commands:
cdk synth
npx cfn-idle-cost
cfn-idle-cost reads your cdk.out and prints the idle cost of the app: what you pay even when nobody uses it.
Why idle cost?
Most surprise AWS bills I have seen were not caused by traffic. They were caused by things that bill by the hour whether or not anyone uses them:
- a NAT Gateway in every AZ of a dev VPC
- interface endpoints, billed per AZ
- an ALB in front of a service that gets ten requests a day
- an RDS instance someone set to Multi-AZ "to be safe"
- an Aurora Serverless v2 cluster that never scales below its minimum ACU
- KMS keys, Secrets Manager secrets and CloudWatch alarms that add up one dollar at a time
In CDK, a single new ec2.Vpc(this, 'Vpc') creates NAT Gateways for you. That default is reasonable for production, but it is easy to miss when you copy the same stack into a sandbox account or a per-developer environment.
Cost Explorer tells you about this after the bill arrives. The AWS Pricing Calculator can tell you before, but you have to enter every resource by hand. cfn-idle-cost reads the template you already have.
What it does
-
Reads
cdk.outdirectly, including Stages and nested stacks. Plain CloudFormation templates (JSON or YAML) work too. -
Names each line by construct path (
Vpc/PublicSubnet1/NATGateway), so you know which line of CDK code to change. -
Runs offline with no AWS credentials. Prices come from a snapshot of the public AWS Bulk Price List bundled in the package and refreshed weekly. The same
cdk.outgives the same numbers for a given version. Pass--liveif you want current prices from the Price List Query API instead. -
Never guesses. If a value can't be read from the template, such as a Parameter without a default or a reference to another stack, the resource is listed under
? could not estimatewith the reason. A Parameter with aDefaultis estimated with that default, and the line says so.
It covers more than 70 resource types: NAT Gateways, EIPs, VPC endpoints, load balancers, EC2 and Auto Scaling groups, RDS and Aurora, OpenSearch, ElastiCache, DynamoDB provisioned capacity, Kinesis, Fargate services, EFS and FSx, EKS, Network Firewall, WAF, Lambda provisioned concurrency, Transit Gateway attachments, MWAA, and more. The full list is in the README.
Usage-only charges are left out on purpose: S3, Lambda invocations, Aurora storage and I/O, CloudWatch Logs, data transfer, requests. Those depend on traffic, and this tool answers a different question: what is the floor?
Catch it in the pull request
Knowing the current idle cost is useful. Knowing that a pull request is about to add $300 a month is more useful.
--base compares two synthesized apps and prints only what changes:
npx cfn-idle-cost --base cdk.out.main cdk.out
ap-northeast-1 · idle ≈ $382 → $682/mo (+$300)
~ DataStack Db $42 → $322 +$280 db.t4g.micro postgres Multi-AZ → db.t4g.large postgres Multi-AZ
+ NetworkStack Vpc/PublicSubnet2/NATGateway $0.00 → $45 +$45 NAT Gateway
- DataStack Stream $28 → $0.00 -$28 provisioned shards
+ NetworkStack Vpc/PublicSubnet2/EIP $0.00 → $3.65 +$3.65 public IPv4 address
There is a GitHub Action that posts this as a pull request comment, updates it on every push, and fails the check when the increase is over your budget:
name: idle-cost
on: pull_request
permissions: { contents: read, pull-requests: write }
jobs:
idle-cost:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with: { path: head }
- uses: actions/checkout@v7
with: { path: base, ref: "${{ github.base_ref }}" }
- uses: actions/setup-node@v7
with: { node-version: 22 }
- run: npm ci && npx cdk synth -q
working-directory: head
- run: npm ci && npx cdk synth -q
working-directory: base
- uses: badmintoncryer/cfn-idle-cost@v1
with: { base: base/cdk.out, path: head/cdk.out, max-increase: 50 }
Now the reviewer sees "+$280/mo: db.t4g.micro → db.t4g.large" next to the diff, instead of finding out from the bill a month later. Commit cdk.context.json so the synth doesn't need AWS credentials for lookups.
Outside GitHub Actions, --max <usd> and --max-increase <usd> make the CLI exit with code 3 when the budget is exceeded, so it fits into any CI.
What the numbers mean
- On-demand list prices in USD, 730 hours a month.
- Savings Plans, Reserved Instances and the free tier are ignored.
- EC2 is priced as Linux with shared tenancy, because the AMI is not read.
- An Auto Scaling group uses
DesiredCapacitywhen set, otherwiseMinSize.
In other words, it is the price list applied to your template, not a forecast of your bill. If you have a Savings Plan, your real number will be lower. That is fine: the point is to see what you are paying for and which construct it comes from.
Try it
cdk synth
npx cfn-idle-cost
You can also install it with Go (go install github.com/badmintoncryer/cfn-idle-cost@latest), and cdk-idle-cost is the same tool under another name. The npm packages cover Linux and macOS; on Windows, use go install or the binary from the GitHub Release.
I'd love to hear what number your app prints. If it lists a resource under ! not counted yet that matters to you, or if a price looks wrong, please open an issue. And if it saves you a NAT Gateway or two, a ⭐ on GitHub helps others find it.
I also maintain two other tools that read cdk.out before you deploy: cdk-preflight fails cdk synth on templates that are known to fail at deploy time, and cfn-exec-policy generates a least-privilege IAM policy for the CloudFormation execution role, so cdk bootstrap doesn't have to use AdministratorAccess. More on those in later posts.
Top comments (0)