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.
| Meter | Rate | What moves it |
|---|---|---|
| S3 to EC2, same Region | $0.00 per GB | Runner placement |
| Data transfer out to the internet | $0.09 per GB after 100 GB free per month | Runner placement |
| Data transfer out to another AWS Region | $0.02 per GB | Bucket and stack Region drift |
| Managed NAT data processing | $0.045 per GB, plus $0.045 per hour | Static IPs on runners |
| GET and SELECT requests | $0.0004 per 1,000 | Number of objects read per job |
| PUT, COPY, POST and LIST requests | $0.005 per 1,000 | Multipart part size, cache writes |
| S3 Standard storage, first 50 TB | $0.023 per GB-month | Retention 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-use1The 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-0ghi789Runner labels and rates used above
The Linux x64 labels in the workflow bill at the per-minute rates on 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 |
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.
| Input | Value | Source |
|---|---|---|
| Workflow runs per weekday | 60 | Your workflow run history |
| Weekdays per month | 22 | Calendar |
| Bundle written per run | 0.8 GB | Your bucket metrics |
| Bundle read by downstream jobs | 3 reads, 2.4 GB per run | Your workflow graph |
| Fixture shards read by test jobs | 2 reads, 2.2 GB per run | Your bucket metrics |
| Deploy bundle read per run | 0.6 GB | Your workflow graph |
| GET-class requests per run | 2,000 | S3 Storage Lens |
| PUT-class requests per run | 400 | S3 Storage Lens |
| S3 to EC2, same Region | $0.00 per GB | Amazon S3 pricing |
| Transfer out to the internet | $0.09 per GB after 100 GB free | Amazon S3 pricing |
| Transfer out to another Region | $0.02 per GB | Amazon S3 pricing |
| S3 Standard storage | $0.023 per GB-month | Amazon S3 pricing |
| Managed NAT data processing | $0.045 per GB | Amazon VPC pricing |
| Managed NAT hourly charge | $0.045 per hour | Amazon 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 placement | Transfer | Managed NAT | Requests and storage | Monthly total |
|---|---|---|---|---|
| Outside your account, reading over the internet path | 6,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.00 | 7,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.