Track binary size per commit in GitHub Actions
buildstats charts the size of the binary your CI builds, per commit and per target, in your README and comments the delta on pull requests. Free for public repos, 50,000 points per metric; private $9/month.
Binary size creeps. A new dependency adds 300 kB, a generic gets monomorphised forty times, debug info leaks into a release profile, and six months later nobody knows which commit did it. Recording the size on every push to the default branch turns that into a chart with a step in it, and the pull request comment catches the next one before it merges.
Setup: build, stat, push
name: Build
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
id-token: write
pull-requests: write
jobs:
build:
strategy:
matrix:
target: [x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-pc-windows-msvc]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup target add ${{ matrix.target }}
- run: cargo build --release --target ${{ matrix.target }}
- id: size
shell: bash
run: echo "bytes=$(stat -c %s target/${{ matrix.target }}/release/mytool*)" >> "$GITHUB_OUTPUT"
- uses: bitgate/buildstats@v1
with:
variant: ${{ matrix.target }}
metrics: |
binary_size=${{ steps.size.outputs.bytes }} bytes lower
max-regression: 3
The variant input stores every metric as binary_size/<target>, so the three targets draw as three lines on one chart and the pull request comment lists each one. Matrix jobs share a single comment. For Go, go build -ldflags="-s -w" then stat the output; for C++ or Zig, stat whatever the linker wrote; for a stripped and an unstripped build, push both as binary_size/stripped and binary_size/full.
On macOS runners stat -c is not available; use stat -f %z or wc -c < file, which works everywhere.
WebAssembly, APKs and other outputs
The same step covers anything with a size. For a wasm module push the raw and the gzipped size, since the gzipped number is what the browser downloads: wasm=$(wc -c < pkg/app_bg.wasm) and wasm_gzip=$(gzip -9c pkg/app_bg.wasm | wc -c). For Android, stat the APK or AAB from the Gradle output directory; for iOS, the IPA. Container images have their own page: track Docker image size.
A project holds 100 metrics, so the binary, its wasm build, the installer and the compressed download can all live together with the test count and the compile time. Any numeric value works, with a unit of up to 16 characters.
The pull request comment
On pull_request runs the Action compares each metric with the latest value on the base branch and posts one comment per project with the base value, the pull request value and the delta in bytes and percent, marked better or worse according to the lower direction. max-regression: 3 fails the step when any target grew by more than 3%; the comment and the job summary are written first, so the numbers are visible on the red check. Metrics with no base value yet never trip the gate.
Fork pull requests get no OIDC token. To record them, upload the sizes as an artifact and push them from a workflow_run workflow with the sha input set to the head commit.
The chart in the README
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://buildstats.io/acme/mytool/binary_size.svg?theme=dark">
<img alt="binary size per target over time" src="https://buildstats.io/acme/mytool/binary_size.svg">
</picture>
The chart at /binary_size.svg overlays every variant; /binary_size/x86_64-unknown-linux-gnu.svg shows one. n picks 30 to 1,000 points (default 200), w picks 400 to 1,200 pixels (default 800) and ref=release shows a branch other than the default. The badge at /binary_size/x86_64-unknown-linux-gnu/badge.svg shows the latest value in the shields.io flat style. Public images are cached for 5 minutes.
Limits and pricing
| Public repositories | Private repositories | |
|---|---|---|
| Price | Free | $9/month per GitHub user or organization |
| Trial | Not needed | 14 days, no card |
| Projects per owner | 100 | 25 |
| Metrics per project | 100 | 100 |
| Points per metric and branch | 50,000 | 50,000 |
| Variants on one chart | 8 colours | 8 colours |
| Metrics per push | 50 | 50 |
| Members | Unlimited | Unlimited |
Everything else is on the pricing page.
Other ways to do this
cargo-bloat and twiggy explain where the bytes went, but run locally and store nothing. github-action-benchmark can record a custom "smaller is better" number in a gh-pages branch, with commit comments rather than pull request comments. Bencher has a file size adapter and a hosted dashboard, free for public projects and from $100/month otherwise. For a chart in the README and a pull request delta with one step and no branch to push to, buildstats is the shortest path.
Questions
Does buildstats need the binary?
No. Only the number is sent: a metric name, a value and an optional unit. The binary stays in your runner.
Can I track several targets on one chart?
Yes. The variant input stores binary_size/linux, binary_size/macos and binary_size/windows as separate series that overlay on one chart with up to 8 colours. Each also has its own chart and badge URL.
How do I measure a stripped binary?
Strip it in the workflow first, with strip or with -ldflags -s -w for Go and strip = true in the Cargo release profile, then stat the result. Push the unstripped size as a second metric if you want both.
Does the pull request comment work for matrix builds?
Yes. All matrix legs update the same comment, one row per metric and variant.
Can I fail the build when the binary grows?
Set max-regression on the step. The step fails when a metric marked lower got worse than the base branch by more than that percentage, after writing the comment and the job summary.