Home › Decoder › TimedStorage-ByteHrs on ECR - $0.10 per GB-month for container images
TimedStorage-ByteHrs on ECR - $0.10 per GB-month for container images
TimedStorage-ByteHrs under Amazon ECR is the charge for container images stored in your repositories, $0.10 per GB-month in US East (N. Virginia).
In your bill the usage type reads: TimedStorage-ByteHrs, USE1-TimedStorage-ByteHrs
What it costs
| Charge | Price | Unit |
|---|---|---|
| ECR image storage (TimedStorage-ByteHrs) | $0.10 | per GB-month |
US East (N. Virginia); other regions differ. Checked 2026-11-11. Sources: Amazon ECR pricing, AWS price list file, Amazon ECR offer, us-east-1.
Example with made-up numbers
3,000 GB of container images stored in the month
3,000 x $0.10 = $300.00 per month on the ECR TimedStorage-ByteHrs line
A lifecycle policy that expires untagged images and keeps the newest 20 per repository removes 2,100 GB and saves 2,100 x $0.10 = $210.00 per month
Typical causes
An image per commit
CI pushes a fresh multi-hundred-megabyte image on every commit or pull request and nothing removes the old ones.
Untagged leftovers
Every re-push of latest or a mutable tag leaves the previous image untagged but stored.
Retired services
Repositories for services nobody runs any more.
Large images
Build tools and caches shipped in the final image instead of a multi-stage build.
Find it in your bill
Have a CUR file queryable in Athena? Paste this and run it:
SELECT date_trunc('month', line_item_usage_start_date) AS month,
line_item_product_code,
line_item_usage_type,
line_item_resource_id,
SUM(line_item_usage_amount) AS usage_amount,
SUM(line_item_unblended_cost) AS cost
FROM your_cur_table
WHERE line_item_product_code = 'AmazonECR'
AND (line_item_usage_type LIKE '%TimedStorage-ByteHrs')
GROUP BY 1, 2, 3, 4
ORDER BY month DESC, cost DESC
LIMIT 50;Columns used: line_item_usage_start_date, line_item_product_code, line_item_usage_type, line_item_resource_id, line_item_usage_amount, line_item_unblended_cost.
No CUR file? Use Cost Explorer
- Open Billing and Cost Management, then Cost Explorer, and set the date range to the last 3 months with Monthly granularity.
- Set Group by to Usage type.
- In the Filters panel choose Usage type and tick the entries ending in *TimedStorage-ByteHrs (the region prefix, such as USE1-, differs by region).
- Read the monthly cost per usage type. A line that grows faster than your traffic is the one to chase with the fix below.
- To see which resource drives it, query the CUR above; resource IDs appear only if your CUR was set up with resource IDs included, otherwise the resource column is empty.
Column names checked against: docs.aws.amazon.com/cur/latest/userguide/Lineitem-columns.html, docs.aws.amazon.com/cur/latest/userguide/cur-ate-run.html.
The fix, step by step
Verify first
Check these before you change anything: the steps below are suggestions, and only you can see your own configuration.
- An expired image cannot be restored. Check that no ECS task definition, Lambda function, Kubernetes manifest or rollback script still points at an old tag or digest.
- Preview the policy first (start-lifecycle-policy-preview) and read the list of images it would expire.
- Keep enough tagged images for your longest rollback: if you can roll back 10 releases, keep more than 10.
- Lifecycle policies run in the background; the storage line drops over the next days, not instantly.
- Repositories replicated to other Regions bill in each Region; set the policy on the replica too.
In the console
- ECR console > Repositories; open the biggest repositories and sort Images by Pushed at.
- Note how many images are untagged and how many tags are older than your longest rollback window.
- Repository > Lifecycle Policy > Create rule: Image status Untagged, Since image pushed more than 14 days, Action Expire.
- Add a second rule for tagged images: Image count more than 20, Action Expire.
- Use the preview in the lifecycle editor to see which images would expire before you save.
With the CLI
aws ecr describe-repositories --query 'repositories[].repositoryName' --output text
aws ecr describe-images --repository-name EXAMPLE --query 'sort_by(imageDetails,&imagePushedAt)[].[imagePushedAt,imageSizeInBytes,imageTags]' --output table
aws ecr put-lifecycle-policy --repository-name EXAMPLE --lifecycle-policy-text '{"rules":[{"rulePriority":1,"description":"expire untagged after 14 days","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countUnit":"days","countNumber":14},"action":{"type":"expire"}}]}'aws ecr start-lifecycle-policy-preview --repository-name EXAMPLE --lifecycle-policy-text file://policy.json
aws ecr delete-lifecycle-policy --repository-name EXAMPLE
Rollback
Delete the lifecycle policy to stop further expiry. Images already expired must be rebuilt and pushed again from source.
Find your ECR storage line in your own bill
Drop in your Cost Explorer or CUR CSV. Billtrim reads it in your browser, finds the ECR storage line and shows how many gigabytes of images you pay for.
Paste your export: how to export a Cost Explorer CSV or CUR file in two minutes.
Questions people ask
Is a lifecycle policy free?
Yes. ECR lifecycle policies have no extra charge; you only stop paying for the storage of the images they expire.
Do layers shared between images count once?
Layers are stored once per repository, so many similar images share storage; deleting an image frees only the layers no other image uses.
Does this usage type appear for other services?
Yes: S3, EFS and CloudWatch use the same name, so Billtrim checks the service column before reading it as ECR.
Related
- TimedStorage-ByteHrs: the S3 Standard storage line
- EBS:SnapshotUsage - the snapshot storage line, and how to trim it
- Fargate-vCPU-Hours:perCPU and Fargate-GB-Hours - Fargate task cost, and the cheaper arm64
Part of the guide: Reduce your AWS bill as a startup: a checklist for teams with no FinOps person