Skip to main content

costwatch

CI License

Find wasted AWS spend, and who left it running.

costwatch scans an AWS account for idle and orphaned resources, estimates what each one costs per month, and (with --owners) works out who owns it from tags or CloudTrail.

$ costwatch scan --owners          # example output, abridged
Account 123456789012
 $/month  Check                  Region     Resource       Details                                   Owner
   50.00  unattached-ebs-volume  eu-west-1  old-db-data    500 GiB gp2 volume not attached to any…   alice@example.com (AWSReservedSSO_Admin)
   16.43  idle-load-balancer     eu-west-1  staging-alb    application load balancer has no regis…   deploy-bot
    9.50  old-rds-snapshot       eu-west-1  pre-upgrade    100 GiB manual snapshot of postgres in…   dba-team  (tag:Team)
    3.65  unused-elastic-ip      us-east-1  eipalloc-…     Elastic IP 3.250.1.10 is not associated…  unknown

Waste by owner
  alice@example.com (AWSReservedSSO_Admin)   1   50.00
  deploy-bot                                 1   16.43
  ...
Total: ~$79.58/month (~$955/year) across 17 region(s)
  • Read-only. It only calls Describe*/Lookup* APIs. Run costwatch policy to see exactly what it needs.
  • Local. It runs on your machine with your credentials. Nothing is sent anywhere.

Install

Requires Python 3.12+.

uv tool install git+https://github.com/jonmelia/costwatch
# or
pipx install git+https://github.com/jonmelia/costwatch

Usage

costwatch scan                                  # all enabled regions, default credentials / $AWS_PROFILE
costwatch scan --profile prod --region eu-west-1 --region us-east-1
costwatch scan --owners                         # add who owns each resource
costwatch scan --min-cost 5                     # hide findings under $5/month
costwatch scan --snapshot-age-days 180 --stopped-days 60
costwatch scan --idle-days 30                   # look at 30 days of CloudWatch metrics
costwatch scan --json > findings.json           # machine-readable
costwatch policy                                # IAM policy for a read-only role

If a check isn't permitted or a region is disabled, it's reported at the end and the rest of the scan carries on.

Checks

Check Flags Estimated cost
unattached-ebs-volume EBS volumes not attached to an instance Storage + provisioned IOPS/throughput
old-ebs-snapshot EBS snapshots older than 90 days that no AMI uses Upper bound (snapshots are incremental)
gp2-volume Attached gp2 volumes Saving from switching to gp3 at the same performance
unused-elastic-ip Elastic IPs not associated with anything $0.005/hour
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 registered targets Hourly load balancer charge
idle-nat-gateway NAT gateways that sent under 1 MiB in 14 days $0.045/hour (plus any data)
old-rds-snapshot Manual RDS/Aurora snapshots older than 90 days Upper bound: allocated size × backup rate
idle-rds-instance RDS instances with no connections in 14 days Instance (× 2 for Multi-AZ) + storage
retained-rds-backup Automated backups kept after their instance was deleted Upper bound: allocated size × backup rate
log-group-no-retention CloudWatch log groups over 1 GiB with no retention set Current storage at $0.03/GB-month

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.

Prices are us-east-1 on-demand approximations (pricing.py). They're for ranking waste, not for matching your bill to the cent; other regions are usually within about 20%. Instance types missing from the price table are still reported, with a note and a cost of $0.

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 resources show as unknown unless they're tagged. Lookups are rate-limited by AWS (2 per second per region), so this is slower on accounts with lots of findings.

Development

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

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

Adding a check

  1. Write a function (session, region, config) -> list[Finding] in src/costwatch/checks/ and add it to ALL_CHECKS.
  2. Add any new permissions to iam.py.
  3. If it should support --owners, add its create event(s) to CREATE_EVENTS in owners.py.
  4. Test it with moto.

Contributions welcome: open an issue first for anything large.

License

Apache 2.0

Metadata

Release files for costwatch 0.1.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 0.1.0
File Size Uploaded
costwatch-0.1.0.tar.gz 20.9 kB Details

Built distribution (wheel)

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

Total release size: 49.2 kB

Release files / costwatch-0.1.0.tar.gz

Download URL costwatch-0.1.0.tar.gz
Size 20.9 kB
Tags Source
SHA-256 checksum
How to use checksums
27e653e87d87ef55010d5844e7576f33781d2948f0b5351bf3738872c2cbd221
BLAKE2b-256 checksum
How to use checksums
429a82caebfe96d54ea59e89dcf96203786c58a7578dd7ae1402e96c11d2f34c
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-0.1.0-py3-none-any.whl

Download URL costwatch-0.1.0-py3-none-any.whl
Size 28.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b87435faa54e5b49ae8acb66cf700cf85e370192add60319772141f2ecf35a0d
BLAKE2b-256 checksum
How to use checksums
4c28c357abb7b01230cb8f7e03bc1fdb8737ae32bbe8b412611114c48fbda30e
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

1.0.1

2 release files

1.0.0

2 release files

This release

0.1.0 This release

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