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 pipelineImage pulls per mergeBytes at 1.2 GB per pull
build and push0, it pushes0 GB
integration tests, 4 matrix legs44.8 GB
container image scan11.2 GB
smoke test against staging11.2 GB
release promotion by pull and retag11.2 GB
Total per merge78.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,billing

Promote 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-use1

Labels and rates used above

Linux x64 labels from the WarpBuild catalog, with per-minute rates from the pricing page:

Runner labelOSvCPURAMStoragePrice per minute
warp-ubuntu-latest-x64-2xUbuntu 24.0428 GB150GB SSD$0.004
warp-ubuntu-latest-x64-4xUbuntu 24.04416 GB150GB SSD$0.008
warp-ubuntu-latest-x64-8xUbuntu 24.04832 GB150GB SSD$0.016
warp-ubuntu-latest-x64-16xUbuntu 24.041664 GB150GB SSD$0.032
warp-ubuntu-latest-x64-32xUbuntu 24.0432128 GB150GB 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.

InputValueSource
Merges to main per weekday15Your workflow run history
Weekdays per month22Calendar
Image pulls per merge7The pipeline table above
Compressed image size1.2 GBYour registry metrics
Pulling job duration7 minutesYour workflow run history
Retained image storage in ECR40 GBYour registry metrics
ECR storage rate$0.10 per GB-monthAmazon ECR pricing
ECR to same-Region compute$0.00 per GBAmazon ECR pricing
Cross-Region and internet transfer$0.09 per GBAmazon ECR pricing
Free internet transfer allowance100 GB per month, all servicesAmazon EC2 On-Demand pricing
Cross-Availability-Zone transfer$0.01 per GB each directionAmazon EC2 On-Demand pricing
Managed NAT data processing$0.045 per GBAmazon VPC pricing
Managed NAT hourly charge$0.045 per hourAmazon VPC pricing
GitHub-hosted ubuntu-latest, private repositories$0.006 per minuteGitHub 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 placementMonthly pull chargeFixed chargeMonthly total
Outside the Region, over the internet path2,672 GB billable times $0.09 equals $240.48$0.00$240.48
Same Region, private subnets behind managed NAT2,772 GB times $0.045 equals $124.74730 hours times $0.045 equals $32.85$157.59
Same Region, crossing one Availability Zone boundary2,772 GB times $0.01 equals $27.72$0.00$27.72
Same Region, same zone, public subnets2,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.

ChangePulls per mergeGB per monthManaged NAT monthly total
Starting point, 1.2 GB image72,772$157.59
Collapse 4 matrix legs, retag without pulling31,188$86.31
Also cut the image to 0.4 GB3396$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.