What Pulling Images from ECR Costs Your Pipeline
Pulls from ECR bill your AWS account rather than the GitHub invoice. Where registry charges land in a deploy pipeline, plus a worked monthly model.
Last verified:
Pulling a container image out of ECR inside a GitHub Actions job costs you twice: once for the bytes crossing a metered network path in your AWS account, and once for the storage holding every retained image version at $0.10 per GB-month (Amazon ECR pricing, checked on 2026-08-13). None of it appears on the GitHub invoice, which is why a registry line can grow for a year before anyone attributes it to a workflow. This guide shows which paths are metered, which pulls in a normal deploy pipeline are avoidable, and what the monthly total looks like with every assumption written down.
Diagnosis
Registry charges split into three network shapes and one storage shape. Which network shape you are paying depends entirely on where the runner sits relative to the registry, and none of that is visible from a workflow file.
Cross-availability-zone pulls
Inside a single Region, a pull can still cross an Availability Zone boundary on its way from the registry path to the instance running the job. AWS prices data transferred across Availability Zones in the same Region at $0.01 per GB in each direction (Amazon EC2 On-Demand pricing, checked on 2026-08-13).
That rate looks small until you multiply it by pull count. It is also the charge that appears when a self-hosted fleet spreads across three zones for instance availability while the resources it reads sit in one. Check which path your pulls take before you assume this line is zero.
Managed NAT data processing
Runners that need a stable outbound IP address get placed in private subnets, and everything they move then leaves through a managed NAT service. In US East (N. Virginia), AWS charges $0.045 per hour for the service plus $0.045 per GB of data processed, in both directions (Amazon VPC pricing, checked on 2026-08-13).
This is the largest surprise on most bills, because it prices bytes that AWS would otherwise carry at $0.00 per GB. A 1.2 GB image pulled 105 times a day moves 126 GB a day through a meter that charges per gigabyte regardless of source or destination. AWS documents private connectivity options for registry traffic inside your own VPC; those belong to your AWS configuration and are priced on the AWS pages linked above.
Cross-region transfer
When the runner sits outside the Region that holds the registry, or outside the account entirely, the pull leaves the Region. AWS states that data transferred between Amazon ECR and other services in different Regions is charged at internet data transfer rates on both sides of the transfer, and the worked example on that page uses $0.09 per GB (checked on 2026-08-13). The first 100 GB of internet data transfer out per month is free, aggregated across all AWS services and Regions, and production has usually spent that allowance before GitHub Actions asks for any.
By the same pricing page, data transferred between Amazon ECR and other services in the same Region, including Amazon EC2, is free of charge at $0.00 per GB. The workflow file is identical in both cases. Machine placement is the whole difference.
Storage, including pull-through cache
ECR bills $0.10 per GB-month for stored images in private repositories. Every tag your pipeline pushes per merge accumulates, and pull-through cache repositories add upstream public images to the same meter. Teams that adopt pull-through cache to escape upstream rate limits are trading a rate limit for a storage line, which is usually the right trade, and it still belongs in the model.
How many pulls a normal pipeline makes
Count the jobs that actually pull, rather than the merges. A single-image pipeline with one build and a fan-out looks like this per merge:
| Job in the pipeline | Image pulls per merge | Bytes at 1.2 GB per pull |
|---|---|---|
| build and push | 0, it pushes | 0 GB |
| integration tests, 4 matrix legs | 4 | 4.8 GB |
| container image scan | 1 | 1.2 GB |
| smoke test against staging | 1 | 1.2 GB |
| release promotion by pull and retag | 1 | 1.2 GB |
| Total per merge | 7 | 8.4 GB |
Generally available Linux and Windows runners do not have plan-level concurrency caps of WarpBuild runners and capacity adjusts dynamically, so nothing throttles this fan-out on your behalf. The byte count per merge is the number you control.
Confirming it on your own bill
Open Cost Explorer, group by Usage Type, and filter to the account or tag that carries the runner fleet. Four usage types cover the shapes above: NatGateway-Bytes, DataTransfer-Regional-Bytes, DataTransfer-Out-Bytes, and the ECR TimedStorage-ByteHrs line. If the daily curve rises with merge volume and stays flat when end-user traffic moves, the pipeline owns that spend.
Fix
Three levers, in the order that pays back fastest. The general egress picture across artifacts and caches is covered in GitHub Actions egress costs; this section stays on the registry.
Ship fewer bytes per pull
A multi-stage Dockerfile that copies only the compiled output into a slim runtime base cuts what every future job pulls, permanently. Pin base images by digest so an upstream retag stops invalidating layers across the fleet, and keep test fixtures and sample data out of the runtime image so the test legs stop paying to download them.
Order layers so the volatile ones sit last. Pulls fetch only the layers the runner lacks, so stable base layers stay cached on a warm machine while the application layer is the only thing that moves.
Reuse layers instead of repulling them
Push the layer cache to a cache tag in the same registry as the image, so unchanged layers stop moving on every build. Remote Docker builders keep a persistent layer cache on the builder itself, which takes the cache traffic off the runner path entirely; the setup is in the Docker builders documentation, and WarpBuild also ships snapshot runners, GitHub Actions observability, an MCP server, and the Action Debugger around the same workflows.
Then cut redundant pulls. Four matrix legs on four machines pull four copies of the same image; running the same suites in one job on a larger runner pulls once. Promotion is another free win, because a tag can be moved with the ECR API against the existing manifest rather than a pull followed by a push. More on the pull-time side of this in how to reduce Docker image pull time in GitHub Actions.
Put the registry and the runners in the same Region
This is the structural fix, and by AWS's own published rate it takes the transfer line to $0.00 per GB. WarpBuild BYOC runs runners inside your own AWS, GCP, or Azure account, and Terraform support exists for BYOC on AWS, so the placement lives in the same module as the rest of your infrastructure. The AWS BYOC configuration guide lists the VPC, subnet, security group, and quota prerequisites, plus the bucket the stack uses for cache and telemetry.
Colocation leaves one residue: runners in private subnets still pay managed NAT data processing on everything they move, package registries and the GitHub API included. Keep the fleet in public subnets unless an allowlisted static IP is a hard requirement for that workflow.
Teams that want registry pull charges off the bill entirely qualify on the enterprise tier, where runners pull large images and artifacts from ECR, S3, and similar stores with zero egress cost to you, whether your runners live in your cloud or ours; the scope is on zero egress for the enterprise tier.
Configuration
Build once, push once
The build job below pushes through a remote Docker builder profile and exports the digest for downstream jobs, so nothing after it needs to resolve a tag.
name: deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: warp-ubuntu-latest-x64-4x
permissions:
id-token: write
contents: read
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/github-actions-deploy
aws-region: us-east-1
- uses: aws-actions/amazon-ecr-login@v2
id: ecr
- uses: Warpbuilds/build-push-action@v6
id: push
with:
context: .
push: true
profile-name: api-builder
tags: ${{ steps.ecr.outputs.registry }}/api:${{ github.sha }}Collapse four pulls into one
Four matrix legs pull 4.8 GB per merge at a 1.2 GB image. The same suites in one job on an 8 vCPU runner pull 1.2 GB and finish with the same coverage.
integration:
needs: build
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/github-actions-deploy
aws-region: us-east-1
- uses: aws-actions/amazon-ecr-login@v2
id: ecr
- run: docker pull "${{ steps.ecr.outputs.registry }}/api@${{ needs.build.outputs.digest }}"
- run: ./scripts/integration.sh --suites api,worker,web,billingPromote a release without pulling it
The promotion job moves a tag against the manifest already in ECR, so it transfers kilobytes instead of gigabytes.
promote:
needs: [build, integration]
runs-on: warp-ubuntu-latest-x64-2x
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/github-actions-deploy
aws-region: us-east-1
- name: Retag in place
run: |
MANIFEST=$(aws ecr batch-get-image \
--repository-name api \
--image-ids imageDigest=${{ needs.build.outputs.digest }} \
--query 'images[0].imageManifest' --output text)
aws ecr put-image \
--repository-name api \
--image-tag release \
--image-manifest "$MANIFEST"Move the same jobs into your own account
BYOC runners register with a warp-custom- prefix followed by the runner name chosen in the dashboard. Setting the stack Region to the Region that holds the registry is what turns the pull into same-Region traffic.
integration:
runs-on: warp-custom-deploy-use1Labels and rates used above
Linux x64 labels from the WarpBuild catalog, with per-minute rates from the pricing page:
| Runner label | OS | vCPU | RAM | Storage | Price per minute |
|---|---|---|---|---|---|
| warp-ubuntu-latest-x64-2x | Ubuntu 24.04 | 2 | 8 GB | 150GB SSD | $0.004 |
| warp-ubuntu-latest-x64-4x | Ubuntu 24.04 | 4 | 16 GB | 150GB SSD | $0.008 |
| warp-ubuntu-latest-x64-8x | Ubuntu 24.04 | 8 | 32 GB | 150GB SSD | $0.016 |
| warp-ubuntu-latest-x64-16x | Ubuntu 24.04 | 16 | 64 GB | 150GB SSD | $0.032 |
| warp-ubuntu-latest-x64-32x | Ubuntu 24.04 | 32 | 128 GB | 150GB SSD | $0.064 |
The full label set is in the cloud runners documentation. BYOC runners are billed at $0.002 per minute in WarpBuild fees on Linux, with the compute billed by your own cloud account.
Cost or Time Model
Assumptions
Every input is stated so you can substitute your own. AWS rates are list prices for US East (N. Virginia), checked on 2026-08-13.
| Input | Value | Source |
|---|---|---|
| Merges to main per weekday | 15 | Your workflow run history |
| Weekdays per month | 22 | Calendar |
| Image pulls per merge | 7 | The pipeline table above |
| Compressed image size | 1.2 GB | Your registry metrics |
| Pulling job duration | 7 minutes | Your workflow run history |
| Retained image storage in ECR | 40 GB | Your registry metrics |
| ECR storage rate | $0.10 per GB-month | Amazon ECR pricing |
| ECR to same-Region compute | $0.00 per GB | Amazon ECR pricing |
| Cross-Region and internet transfer | $0.09 per GB | Amazon ECR pricing |
| Free internet transfer allowance | 100 GB per month, all services | Amazon EC2 On-Demand pricing |
| Cross-Availability-Zone transfer | $0.01 per GB each direction | Amazon EC2 On-Demand pricing |
| Managed NAT data processing | $0.045 per GB | Amazon VPC pricing |
| Managed NAT hourly charge | $0.045 per hour | Amazon VPC pricing |
| GitHub-hosted ubuntu-latest, private repositories | $0.006 per minute | GitHub Actions billing |
The arithmetic
Monthly pulls: 15 merges times 7 pulls times 22 weekdays equals 2,310.
Monthly bytes pulled: 2,310 times 1.2 equals 2,772 GB.
Runner minutes on the pulling jobs: 2,310 times 7 equals 16,170 minutes.
Now price the same 2,772 GB by placement.
| Runner placement | Monthly pull charge | Fixed charge | Monthly total |
|---|---|---|---|
| Outside the Region, over the internet path | 2,672 GB billable times $0.09 equals $240.48 | $0.00 | $240.48 |
| Same Region, private subnets behind managed NAT | 2,772 GB times $0.045 equals $124.74 | 730 hours times $0.045 equals $32.85 | $157.59 |
| Same Region, crossing one Availability Zone boundary | 2,772 GB times $0.01 equals $27.72 | $0.00 | $27.72 |
| Same Region, same zone, public subnets | 2,772 GB times $0.00 equals $0.00 | $0.00 | $0.00 |
Row one credits the 100 GB monthly allowance once. If production already consumed it, add $9.00.
Storage sits on top of every row: 40 GB times $0.10 equals $4.00 per month.
What the two fixes are worth
Apply the levers from the Fix section to the managed NAT row, which is the common case for fleets with static IPs.
| Change | Pulls per merge | GB per month | Managed NAT monthly total |
|---|---|---|---|
| Starting point, 1.2 GB image | 7 | 2,772 | $157.59 |
| Collapse 4 matrix legs, retag without pulling | 3 | 1,188 | $86.31 |
| Also cut the image to 0.4 GB | 3 | 396 | $50.67 |
The image size lever and the pull count lever multiply. Getting from $157.59 to $50.67 needed no change of provider, no new tooling, and no reduction in test coverage.
The minute line next to it
At 16,170 minutes a month, GitHub's published rate of $0.006 per minute for ubuntu-latest on private repositories is $97.02 (GitHub Actions billing, checked on 2026-08-13). The same minutes on warp-ubuntu-latest-x64-2x at $0.004 per minute are $64.68 (pricing).
Read those two numbers against the starting registry line of $157.59. The bytes cost more than the compute in this pipeline, and no dashboard inside GitHub shows the byte side at all.
Where WarpBuild fits
WarpBuild pricing is purely usage based. There is no base subscription fee, no platform fee, and no seat fee, so the runner side of the model is the per-minute rates in the label table. Signup includes $10 free credits, which covers about 350 runs of the 7 minute integration job on warp-ubuntu-latest-x64-2x while you compare Cost Explorer before and after. Use the pricing page for the full rate table, and measure both job duration and ECR transfer charges before projecting a fleet total.
For the registry side, BYOC puts the runners in the account and Region that already holds ECR, so pulls become same-Region traffic priced by your cloud provider. The object storage half of the same problem is covered in S3 transfer costs from GitHub Actions.
Cloud list prices change. Re-check the three AWS links and the GitHub billing reference before quoting these totals internally, and re-run the arithmetic with your own image size and pull count.
FAQ
Does GitHub bill us for pulling images from ECR?
No. GitHub bills GitHub Actions minutes and artifact storage. The gigabytes a job pulls out of ECR are metered by AWS, so the charge lands on your AWS bill as data transfer or as managed NAT data processing.
Why is our ECR line larger than the image size suggests?
Two charges stack. Storage is billed at $0.10 per GB-month for every retained image version, and every pull is billed again on whichever network path it takes. A pipeline that pulls one image in seven jobs per merge moves seven copies of it.
Do runners in the same Region as ECR pull for free?
AWS prices data transferred between Amazon ECR and other services in the same Region at $0.00 per GB, checked on 2026-08-13. Runners placed in private subnets still pay managed NAT data processing at $0.045 per GB in US East (N. Virginia) on everything that leaves through that path.
Can registry pull charges reach zero?
On the enterprise tier, runners pull large images and artifacts from ECR, S3, and similar stores with zero egress cost to you, and that applies whether your runners live in your cloud or ours. The scope is on zero egress for the enterprise tier.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.