Skip to main content

costwatch

CI PyPI License

Find wasted AWS spend, and who left it running.

costwatch scans an AWS account for idle and orphaned resources and prices each one with that region's AWS list prices. It can also tell you who owns each resource (from tags or CloudTrail) and whether it's managed by Terraform or CloudFormation, so you know who to ask and how to remove it safely.

$ costwatch scan --region eu-west-2 --tfstate s3://my-tf-state/      # example output
Account 123456789012
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ $/month ┃ Check                 ┃ Region    ┃ Resource              ┃ Details                                ┃ Managed by                    ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│   46.40 │ unattached-ebs-volume │ eu-west-2 │ old-db-data           │ 500 GiB gp3 volume not attached to any │ unmanaged                     │
│         │                       │           │ vol-65e02f208655ea11d │ instance (created 41 days ago)         │                               │
│   19.32 │ idle-load-balancer    │ eu-west-2 │ staging-alb           │ application load balancer has no       │ terraform                     │
│         │                       │           │                       │ registered targets                     │ module.staging.aws_lb.staging │
│    3.65 │ unused-elastic-ip     │ eu-west-2 │ eipalloc-b6c48e0af67… │ Elastic IP 18.130.4.21 is not          │ unmanaged                     │
│         │                       │           │                       │ associated with anything               │                               │
└─────────┴───────────────────────┴───────────┴───────────────────────┴────────────────────────────────────────┴───────────────────────────────┘
Total: ~$69.37/month (~$832/year) across 1 region(s)
Prices: AWS on-demand list prices of 2026-10-08.
  • Read-only. It only calls Describe*, List*, Get* and Lookup* APIs. costwatch policy prints exactly what it needs.
  • Local. It runs on your machine with your credentials. Nothing is sent anywhere.
  • Priced per region from AWS's public price list, refreshed monthly.

Install

Requires Python 3.12+.

uv tool install costwatch     # or: pipx install costwatch
uvx costwatch scan            # or run once without installing

Quick start

costwatch scan                                  # every enabled region, default credentials
costwatch scan --profile prod --region eu-west-2 --region us-east-1
costwatch scan --owners                         # who owns each resource
costwatch scan --tfstate s3://my-tf-state/      # Terraform-managed or not
costwatch scan --format markdown -o waste.md    # for a PR, issue or wiki
costwatch checks                                # list the checks
costwatch policy                                # IAM policy for a read-only role

Checks

Check Flags Estimated monthly cost
unattached-ebs-volume EBS volumes not attached to any instance Storage + provisioned IOPS/throughput
old-ebs-snapshot Snapshots older than 90 days that no AMI uses (AWS Backup/DLM snapshots excluded) Snapshot storage (upper bound; snapshots are incremental)
gp2-volume Attached gp2 volumes The saving from moving to gp3 at the same performance
unused-elastic-ip Elastic IPs not associated with anything Public IPv4 hourly charge
long-stopped-instance Instances stopped for 30+ days Their attached EBS volumes
idle-ec2-instance Running instances whose CPU never went above 5% in 14 days Linux on-demand instance price
unused-ami Your AMIs older than 90 days that no instance or launch template uses Their snapshots (upper bound)
idle-load-balancer ALB/NLB/GWLB/Classic with no targets, or no traffic in 14 days Hourly load balancer charge
idle-nat-gateway NAT gateways that sent under 1 MiB in 14 days Hourly NAT gateway charge
old-rds-snapshot Manual RDS/Aurora snapshots older than 90 days Backup storage (upper bound)
idle-rds-instance RDS instances with no connections in 14 days Instance (by engine, class, Multi-AZ) + storage
retained-rds-backup Automated backups kept after their instance was deleted Backup storage (upper bound)
log-group-no-retention CloudWatch log groups over 1 GiB with no retention set Current log storage

The idle checks use CloudWatch metrics and are deliberately conservative: an instance only counts as idle if its CPU never went above 5%, and resources with no metric data are skipped (except load balancers, which only publish metrics when they have traffic).

Tune them with --snapshot-age-days, --stopped-days and --idle-days, and pick checks with --checks a,b or --skip-checks a,b.

Prices

Each finding is priced with its own region's on-demand list prices: EBS by volume type, IOPS and throughput; EC2 by instance type; RDS by engine, class and Multi-AZ; load balancers, NAT gateways, snapshots and log storage. The prices ship inside the package, built from AWS's public price list and refreshed monthly, so a scan needs no pricing permissions and makes no pricing calls.

They're list prices: discounts, Savings Plans, Reserved Instances and the free tier aren't applied. Regions without price data (rare; the report says which) use us-east-1 prices.

Owners (--owners)

For each finding, costwatch looks for an owner in this order:

  1. Tags: Owner, CreatedBy/created-by/created_by, Contact, Team (any case).
  2. CloudTrail: the identity that created the resource. SSO users show as user@example.com (PermissionSetRole), IAM users by name.

CloudTrail event history only goes back 90 days, so older untagged resources show as unknown. Lookups are rate-limited by AWS (2 per second per region), so this is slower on accounts with many findings.

Terraform and CloudFormation (--tfstate)

Pass your Terraform state and every finding is marked terraform (with its address), cloudformation (from the stack tag) or unmanaged:

costwatch scan --tfstate s3://my-tf-state/                 # every *.tfstate under the prefix, incl. workspaces
costwatch scan --tfstate s3://my-tf-state/prod/network.tfstate
costwatch scan --tfstate ./infra                           # every *.tfstate in a directory
terraform state pull > app.tfstate && costwatch scan --tfstate app.tfstate   # any other backend
  • Unmanaged and idle usually means someone created it by hand and forgot it: the safest thing to clean up.
  • Managed and idle: the recommendation changes to removing it from the code (e.g. module.vpc.aws_nat_gateway.this["eu-west-2a"]) and running terraform apply, because deleting it directly would cause drift.

Only managed resources count; data sources just read existing infrastructure. Volumes attached through aws_instance block devices are matched too. Reading state from S3 needs s3:ListBucket and s3:GetObject on the state bucket (plus kms:Decrypt for a customer-managed key). State files can contain secrets: costwatch only reads identifier attributes, in memory, and never prints anything else from them.

Ignoring findings

Some waste is deliberate. To skip a resource:

  • tag it costwatch:ignore=true (any value except false/no/0), or
  • pass --ignore <id, ARN or name> (repeatable), or
  • list IDs in a file, one per line (# for comments), and pass --ignore-file .costwatchignore.

The report says how many findings were ignored. --min-cost 5 hides anything under $5/month.

Output and CI

costwatch scan --format json  > findings.json   # schema_version 1; stable field names
costwatch scan --format csv   > findings.csv    # for spreadsheets
costwatch scan --format markdown -o waste.md    # for PR comments, issues, wikis
costwatch scan --fail-over 100                  # exit 1 if waste is $100/month or more

Exit codes: 0 scan finished, 1 waste at or over --fail-over, 2 error (credentials, permissions for the whole scan, bad arguments). Problems with individual checks or regions, such as a missing permission, are listed at the end of the report and don't stop the scan.

Example GitHub Actions job, using a read-only role via OIDC:

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/costwatch-read
    aws-region: eu-west-2
- run: uvx costwatch scan --format markdown -o waste.md --fail-over 200
- run: cat waste.md >> "$GITHUB_STEP_SUMMARY"
  if: always()

Other accounts

costwatch scan --role-arn arn:aws:iam::111122223333:role/costwatch-read --external-id <id>

Create the role in the target account with the policy from costwatch policy, trusting the account you run costwatch from.

Python API

import boto3
from costwatch import ScanConfig, scan

result = scan(
    boto3.Session(profile_name="prod"), regions=["eu-west-2"], config=ScanConfig(idle_days=30)
)
for finding in result.findings:
    print(finding.check, finding.resource_id, round(finding.monthly_cost, 2))

Development

git clone https://github.com/jonmelia/costwatch && cd costwatch
uv sync
uv run pytest --cov            # moto-based, no real AWS calls; 100% coverage required
uv run ruff check . && uv run ruff format . && uv run mypy

tests/e2e/ runs the real costwatch command against a moto server. To try it by hand against a fake account full of waste:

uv run moto_server -p 5000
uv run python -m tests.e2e.seed --endpoint http://localhost:5000
export AWS_ACCESS_KEY_ID=testing AWS_SECRET_ACCESS_KEY=testing
AWS_ENDPOINT_URL=http://localhost:5000 \
  uv run costwatch scan --region eu-west-1 --snapshot-age-days 0 --stopped-days 0 --min-cost 1

See CONTRIBUTING.md for adding a check, and CHANGELOG.md for what's changed.

License

Apache 2.0

Metadata

Release files for costwatch 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for costwatch 1.0.0
File Size Uploaded
costwatch-1.0.0.tar.gz 225.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for costwatch 1.0.0
File Interpreter ABI Platform
costwatch-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 457.3 kB

Release files / costwatch-1.0.0.tar.gz

Download URL costwatch-1.0.0.tar.gz
Size 225.3 kB
Tags Source
SHA-256 checksum
How to use checksums
ef0bc6a49dc92a090893a0b377228e0494ca5757ea5580ecb27f84144e264ecd
BLAKE2b-256 checksum
How to use checksums
992b78c9f1e3f34e418d69f1e0d2973a6434996014f6a709c54aaf3b59e55dec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / costwatch-1.0.0-py3-none-any.whl

Download URL costwatch-1.0.0-py3-none-any.whl
Size 232.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4e75801de82b3a6bef17bfa799900e5d48db704a6ef568dbed9b0a3e90e50c3d
BLAKE2b-256 checksum
How to use checksums
72d289cde21e8cb5949711c1a90894174dbfec31ee34f63d1dbd3e58cbe8b4de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page