Modeling S3 Transfer Costs for GitHub Actions

S3 charges for GitHub Actions land on your AWS bill as artifact reads, fixture pulls, and deploy bundles. A worked monthly model with published AWS rates.

Last verified:

Diagnosis

S3 transfer costs on GitHub Actions come from the gigabytes your jobs read out of a bucket, and AWS prices those reads by where the runner sits rather than by how many jobs you run. A runner inside the same AWS Region as the bucket reads at $0.00 per GB, while a runner outside your account reads over the internet path at $0.09 per GB after the first 100 GB each month (Amazon S3 pricing, checked on 2026-08-13).

Before you price anything, find the steps that actually move object storage bytes. Four of them cover most pipelines.

Artifact uploads at the end of the build job

Teams that outgrew actions/upload-artifact push build outputs into their own bucket: a compiled bundle, a container tarball, a set of compiled test binaries. Writes are the cheap direction. AWS charges $0.00 per GB for data transferred into S3 from the internet, so the upload shows up as request charges and as storage that keeps billing until something deletes it.

Artifact downloads in every downstream job

This is the line that grows without anyone deciding to grow it. One build job writes a 0.8 GB bundle, then three downstream jobs each read it back. The write was 0.8 GB and the reads were 2.4 GB, and adding a fourth downstream job adds another 0.8 GB to every run for the life of the workflow.

Dataset fixtures pulled by test jobs

Integration suites that need seed data, recorded HTTP cassettes, or model weights pull them from a bucket at the start of each job. Fixtures are usually larger than build outputs and they change less often, which makes them the best candidate for sharding and for a content-addressed key that a runner-side cache can hold.

Deploy bundles pulled by release jobs

The release job reads the artifact one more time, often from a different workflow than the one that produced it. Deploy frequency drives this line, so it climbs whenever the team merges more often. The registry side of the same story is covered in ECR pull costs on GitHub Actions.

What AWS meters, and what it does not

Four separate meters run at once on a bucket that serves GitHub Actions, and they respond to different levers. Rates below are US East (N. Virginia) list prices from Amazon S3 pricing and Amazon VPC pricing, checked on 2026-08-13.

MeterRateWhat moves it
S3 to EC2, same Region$0.00 per GBRunner placement
Data transfer out to the internet$0.09 per GB after 100 GB free per monthRunner placement
Data transfer out to another AWS Region$0.02 per GBBucket and stack Region drift
Managed NAT data processing$0.045 per GB, plus $0.045 per hourStatic IPs on runners
GET and SELECT requests$0.0004 per 1,000Number of objects read per job
PUT, COPY, POST and LIST requests$0.005 per 1,000Multipart part size, cache writes
S3 Standard storage, first 50 TB$0.023 per GB-monthRetention policy

To confirm the charge belongs to your workflows, open Cost Explorer, group by Usage Type, and filter to the account or tag that carries the runner fleet. Look for DataTransfer-Out-Bytes, DataTransfer-Regional-Bytes, NatGateway-Bytes, and the Requests-Tier1 and Requests-Tier2 usage types. Two signatures identify GitHub Actions as the customer of that line: the daily curve tracks merge volume rather than end-user traffic, and it collapses on weekends.

Fix

Four levers, in the order that pays back fastest.

Read the object once per run

Fan-out is what turns a modest artifact into a large monthly number. Read the bundle once in a setup job, publish the key through job outputs, and let downstream jobs read only what differs. Shard fixtures by test group so each matrix leg pulls its own shard rather than the full dataset, and compress with zstd before upload so every future read moves fewer bytes.

Keep the read inside the Region

This is the structural fix and it is worth more than every workflow tweak combined. A runner in the same account and Region as the bucket reads at AWS's own published $0.00 per GB, so the largest line in the model below disappears. BYOC runs on AWS, GCP, and Azure, and the WarpBuild stack pins a Region at creation time, so the placement decision is made once. Terraform support exists for BYOC on AWS, which keeps that decision in the same module as the VPC and the bucket. Setup steps are in the guide to running GitHub Actions runners in your own AWS account.

Region drift is the failure mode to watch. A bucket in us-east-1 read by runners in us-west-2 is billed as transfer out to another Region at $0.02 per GB, which is quieter than the internet path and still adds up.

Route S3 traffic inside the VPC

The BYOC AWS configuration docs call for routing S3 traffic inside the VPC so runners reach the bucket without incurring data transfer charges, and for the bucket to live in the same Region as the stack. AWS documents this private route to S3 as available at no additional charge, checked on 2026-08-13. Without it, a runner in a private subnet reaches S3 through managed NAT and pays data processing on every gigabyte in both directions.

That is also the reason to leave static IPs off. Enabling them places runners in private subnets behind managed NAT, and the BYOC documentation is explicit that this adds data transfer charges on everything a job moves. Turn static IPs on only for the workflows that need an allowlisted address.

Bound retention

On BYOC, the cache and the runner telemetry live in an object storage bucket in your stack Region. Cache objects land under <bucket>/<org_id>/artifact_cache/<vcs_org>/<vcs_repo>/<vcs_ref>/<version>/<key> and telemetry under <bucket>/runner/logs/all/, and the docs recommend a lifecycle policy with 7 days retention on both. Without one, storage compounds every month while the transfer line stays flat, and the storage meter quietly overtakes it after a year.

Teams that want this line removed entirely qualify for the WarpBuild enterprise tier, where runners pull large artifacts from S3, ECR, and similar stores at zero egress cost during deploys, on both BYOC and WarpBuild-hosted runners; scope is on zero egress on the enterprise tier.

Configuration

A pipeline that reads each object once

The build job uploads a single compressed bundle and publishes its key. Each test leg reads that key once and pulls only its own fixture shard. The runs-on values are the only lines that differ from a GitHub-hosted setup.

name: build-test-deploy
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: warp-ubuntu-latest-x64-4x
    permissions:
      id-token: write
      contents: read
    outputs:
      bundle-key: ${{ steps.pack.outputs.key }}
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/github-actions-ci
          aws-region: us-east-1

      - run: make build

      - id: pack
        run: |
          key="builds/${{ github.sha }}/app.tar.zst"
          tar --zstd -cf app.tar.zst dist
          aws s3 cp app.tar.zst "s3://acme-ci-artifacts/${key}"
          echo "key=${key}" >> "$GITHUB_OUTPUT"

  test:
    needs: build
    runs-on: warp-ubuntu-latest-x64-4x
    strategy:
      matrix:
        shard: [1, 2]
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/github-actions-ci
          aws-region: us-east-1

      - run: aws s3 cp "s3://acme-ci-artifacts/${{ needs.build.outputs.bundle-key }}" app.tar.zst
      - run: aws s3 cp "s3://acme-ci-fixtures/shards/${{ matrix.shard }}/" fixtures/ --recursive
      - run: ./scripts/test.sh --shard ${{ matrix.shard }}

Move the deploy job into the account that holds the bucket

BYOC runners register with a warp-custom- prefix followed by the runner name you chose in the dashboard. Pointing the job at a runner in a stack whose Region matches the bucket is what converts a metered read into same-Region traffic.

  deploy:
    needs: test
    runs-on: warp-custom-ci-use1

The lifecycle policy the docs recommend

Apply 7 days retention to both documented prefixes, and abort incomplete multipart uploads so failed artifact writes stop billing as storage.

{
  "Rules": [
    {
      "ID": "expire-artifact-cache",
      "Status": "Enabled",
      "Filter": { "Prefix": "org_01hq9m4k/artifact_cache/" },
      "Expiration": { "Days": 7 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
    },
    {
      "ID": "expire-runner-telemetry",
      "Status": "Enabled",
      "Filter": { "Prefix": "runner/logs/all/" },
      "Expiration": { "Days": 7 }
    }
  ]
}
aws s3api put-bucket-lifecycle-configuration \
  --bucket warpbuild-ci-use1 \
  --lifecycle-configuration file://lifecycle.json

aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0abc123 \
  --vpc-endpoint-type Gateway \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids rtb-0def456 rtb-0ghi789

Runner labels and rates used above

The Linux x64 labels in the workflow bill at the per-minute rates on 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

BYOC Linux runners carry a $0.002 per minute WarpBuild fee with the compute billed by your own cloud account, and the cache and networking add-ons are included in that rate.

Cost or Time Model

Assumptions

Every input is stated so you can substitute your own. AWS rates are US East (N. Virginia) list prices.

InputValueSource
Workflow runs per weekday60Your workflow run history
Weekdays per month22Calendar
Bundle written per run0.8 GBYour bucket metrics
Bundle read by downstream jobs3 reads, 2.4 GB per runYour workflow graph
Fixture shards read by test jobs2 reads, 2.2 GB per runYour bucket metrics
Deploy bundle read per run0.6 GBYour workflow graph
GET-class requests per run2,000S3 Storage Lens
PUT-class requests per run400S3 Storage Lens
S3 to EC2, same Region$0.00 per GBAmazon S3 pricing
Transfer out to the internet$0.09 per GB after 100 GB freeAmazon S3 pricing
Transfer out to another Region$0.02 per GBAmazon S3 pricing
S3 Standard storage$0.023 per GB-monthAmazon S3 pricing
Managed NAT data processing$0.045 per GBAmazon VPC pricing
Managed NAT hourly charge$0.045 per hourAmazon VPC pricing

Every AWS rate above was checked on 2026-08-13.

The arithmetic

Monthly runs: 60 times 22 equals 1,320.

Bytes read per run: 2.4 plus 2.2 plus 0.6 equals 5.2 GB. Monthly reads: 1,320 times 5.2 equals 6,864 GB. Bytes written per run: 0.8 GB, so monthly writes are 1,056 GB and the total the bucket handles is 7,920 GB.

Requests: 1,320 runs times 2,000 GET-class requests is 2,640,000, which is $1.06 at $0.0004 per 1,000. The PUT side is 528,000 requests, or $2.64 at $0.005 per 1,000. Requests total $3.70 a month.

Storage with the recommended 7 days retention: writes of 48 GB per weekday hold about 240 GB in a rolling window, which is $5.52 a month at $0.023 per GB-month.

Now price the same 6,864 GB of reads three ways.

Runner placementTransferManaged NATRequests and storageMonthly total
Outside your account, reading over the internet path6,764 GB billable times $0.09 equals $608.76$0.00$9.22$617.98
Same account and Region, private subnets, no private S3 route$0.007,920 GB times $0.045 equals $356.40, plus 730 hours times $0.045 equals $32.85$9.22$398.47
Same account and Region, private S3 route in place$0.00$0.00$9.22$9.22

Row one credits the 100 GB monthly allowance once. If production already consumed it, add $9.00. A bucket read across Regions instead of over the internet lands between rows one and two: 6,864 GB times $0.02 equals $137.28 a month.

What the model says

Compute for this pipeline is the smaller number. At 8 minutes of build time per run on warp-ubuntu-latest-x64-4x, 1,320 runs is 10,560 minutes, or $84.48 a month at $0.008 per minute. In row one, the bucket costs more than the runners that read from it, and no dashboard in GitHub Actions shows that line.

Placement moves it and nothing else comes close. Row three is the same workload with the runners inside the account and Region that already holds the bucket, and with the private S3 route the docs ask for.

Retention is the slower failure. Skip the lifecycle policy and the 1,056 GB written each month accumulates: after a year the bucket holds roughly 12,672 GB, which is $291.46 a month in storage on a workload whose transfer line is $0.00.

Scale the model linearly for your own numbers, and remember that fan-out multiplies the read side. Run as many jobs as your workflows need, since generally available Linux and Windows runners do not have plan-level concurrency caps, and count each added leg as another full read of whatever it pulls. The registry pulls and the NAT charges that sit next to this line are covered in GitHub Actions egress costs, explained.

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 this model is the per-minute rates in the table above. Signup includes $10 free credits, which covers 1,250 minutes on warp-ubuntu-latest-x64-4x at $0.008 per minute while you compare Cost Explorer before and after the move.

On the placement side, BYOC puts the runners in your own AWS, GCP, or Azure account, so reads become same-account, same-Region traffic priced by your cloud provider, and the WarpBuild fee stays at $0.002 per minute on Linux. Estimate the result from your storage location, cloud transfer rates, instance prices, and measured job duration.

Cloud list prices change. Re-check the two AWS pricing links before quoting these totals internally, and re-run the arithmetic with your own object sizes and run counts.

FAQ

Does S3 charge for data transfer to GitHub Actions runners?

It depends on where the runner sits. AWS prices data transferred from Amazon S3 to Amazon EC2 in the same Region at $0.00 per GB, and data transferred out to the internet at $0.09 per GB after the first 100 GB each month, aggregated across services. Both figures from Amazon S3 pricing for US East (N. Virginia), checked on 2026-08-13.

Does routing S3 traffic inside the VPC cost anything?

AWS documents this private route to S3 as available at no additional charge, checked on 2026-08-13. The WarpBuild BYOC AWS configuration docs recommend setting it up so runners reach the bucket without incurring data transfer charges.

What does WarpBuild store in the BYOC bucket?

Cache under <bucket>/<org_id>/artifact_cache/<vcs_org>/<vcs_repo>/<vcs_ref>/<version>/<key> and runner telemetry under <bucket>/runner/logs/all/. The docs recommend an S3 lifecycle policy with 7 days retention on both prefixes.

Is cache storage cheaper on BYOC than on WarpBuild-hosted runners?

On hosted runners, cache storage bills at $0.20 per GB-month and cache write or restore operations at $0.0001 each on the pricing page. On BYOC, the cache sits in your own bucket in the stack Region and is billed by your cloud provider, and the add-ons are included in the BYOC rate.

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.