AWS cost · ECR
ECR storage costs $0.10/GB - and CI builds pile it up
ECR storage is the cost your pipeline creates for you. Every push from CI is a new image made of layers, and ECR keeps all of them at $0.10/GB-month. Nothing expires on its own. So the repository that felt free when you set it up grows a little with every merge, and a year of deploys later you are storing hundreds of gigabytes of old builds that nobody will ever pull again. The charge is small per gigabyte, which is exactly why nobody notices it climb.
ECR bills $0.10/GB-month for stored images, and every CI build adds more layers that never delete themselves. Untagged images from overwritten tags pile up invisibly. A single lifecycle policy - expire untagged images after a few days, cap the tagged ones - clears the backlog and keeps it from returning.
How much does ECR storage cost?
At $0.10/GB-month, the bill tracks how much image history you keep - and image layers add up faster than people expect once a base image and dependencies are baked into every build:
| Stored image data | Cost | Per year |
|---|---|---|
| 100 GB stored | $10/month | $120/year |
| 500 GB stored | $50/month | $600/year |
| 2 TB stored | $200/month | $2,400/year |
Why the bill grows: untagged images
The hidden half of ECR storage is untagged images. Push a new build to a tag that already exists - say the latest tag, or a version tag you reuse - and the previous image does not disappear. It loses its tag and stays in the repository as an untagged image, still stored and still billed. A pipeline that deploys many times a day leaves a long trail of them, none visible unless you go looking.
How to see what each repository is storing
List your repositories, then add up the image sizes in the ones that look heavy. Image sizes come back in bytes:
# every repository in the region
aws ecr describe-repositories --query 'repositories[].repositoryName'
# image sizes (bytes) in one repository, newest first
aws ecr describe-images --repository-name my-service \
--query 'reverse(sort_by(imageDetails,&imagePushedAt))[].imageSizeInBytes'
# check whether a lifecycle policy already exists
aws ecr get-lifecycle-policy --repository-name my-serviceThe fix: one lifecycle policy per repository
The lasting fix is a lifecycle policy: expire untagged images after a few days and keep only the last N tagged images per repository. That clears the accumulated layers and stops new ones piling up, without anyone remembering to prune. Applying the policy is a write action, so make it deliberately, once you are happy with the rules:
# WRITE ACTION - applies a policy. Review the JSON first.
aws ecr put-lifecycle-policy --repository-name my-service \
--lifecycle-policy-text file://ecr-lifecycle.jsonWatch the pull path too. Pulling images to a private subnet through a NAT gateway adds data-processing charges on top of storage; a gateway or interface endpoint keeps those pulls off the NAT. And the same accumulate-forever problem hits orphaned AMIs and their snapshots, which are worth pruning in the same pass.
Want to know how much of your bill is image layers, snapshots, and other storage nobody prunes? Connect your account read-only and see what it is wasting, in real dollars.
The role can only read - Get, Describe, List, nothing else. Read the exact permissions before you deploy it, and delete the stack whenever you want.
Frequently asked questions
How much does ECR storage cost?+
Amazon ECR charges $0.10/GB-month for stored images, in both private and public registries. There is no free storage tier on a normal account, so every gigabyte of image layers you keep is billed.
Why does my ECR bill keep growing?+
Every CI build pushes a new image, and each image is a set of layers ECR keeps forever unless you tell it not to. Old and untagged images are never removed automatically, so storage climbs with every deploy until you add a lifecycle policy.
What is an untagged image in ECR?+
When you push a new image to a tag that already exists, the old image loses its tag but stays in the repository as an untagged image - still stored, still billed. A busy pipeline leaves hundreds of them behind.
How do I reduce ECR storage costs?+
Add a lifecycle policy that expires untagged images after a few days and caps how many tagged images each repository keeps. That alone clears most of the accumulated layers and stops them coming back.
Does pulling images from ECR cost extra?+
Storage is only part of it. Pulling an image to a private subnet through a NAT gateway adds NAT data-processing charges on top, and cross-region or internet pulls add data transfer. Keeping pulls in-region and using a gateway or interface endpoint avoids the NAT hop.
Are ECR lifecycle policies safe?+
They are as safe as the rules you write. Expiring untagged images older than a few days is low risk, since they are superseded builds. Be more careful with rules that count tagged images - keep enough recent versions to roll back to before letting older ones expire.
Related cost breakdowns
Orphaned EBS snapshots are quietly draining your AWS bill
EBS snapshots cost $0.05/GB-month and never delete themselves. Forgotten ones pile up into hundreds a month. Here is how to find the orphans and automate cleanup.
Why your S3 bill keeps growing - and how to stop it
Cold backups sit in S3 Standard at ~$46/month per 2 TB when Glacier Deep Archive is ~$2/month for the same files. One lifecycle rule moves them automatically.
Switch EBS gp2 to gp3 and cut 20% with no downtime
gp3 volumes cost $0.08/GB-month against gp2's $0.10 - 20% less at the same or better performance, with a live migration and no downtime. Here is the one command.
The S3 cost you cannot see: incomplete multipart uploads
Failed uploads leave parts that bill every month but never show in the S3 console. A one-line lifecycle rule cleans them up. Here is how to find what is hiding and stop paying for it.