buildstats.io

Track CI build time and test duration per commit in GitHub Actions

buildstats charts how long your compile, test suite or job takes, per commit, in your README and comments the delta on pull requests. Free for public repos, 50,000 points per metric; private $9/month per owner.

GitHub's own Actions metrics show average run times per workflow over a window of up to a year, which answers "is CI slow" but not "which commit made it slow". A timing recorded on every push to the default branch answers that with a chart, and the pull request comment shows a change that doubled the test suite before it lands.

Setup: time the step, push the seconds

Wrap the step you care about with date +%s, or date +%s%N for milliseconds, and push the difference. The example times the compile and the tests separately and records the test count as well.

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  id-token: write
  pull-requests: write

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - id: build
        run: |
          start=$(date +%s%N)
          cargo build --release
          echo "ms=$(( ($(date +%s%N) - start) / 1000000 ))" >> "$GITHUB_OUTPUT"

      - id: test
        run: |
          start=$(date +%s%N)
          cargo test --release 2>&1 | tee test.log
          echo "ms=$(( ($(date +%s%N) - start) / 1000000 ))" >> "$GITHUB_OUTPUT"
          echo "count=$(grep -oP '^test result: ok\. \K[0-9]+' test.log | paste -sd+ | bc)" >> "$GITHUB_OUTPUT"

      - uses: bitgate/buildstats@v1
        with:
          metrics: |
            build_time=${{ steps.build.outputs.ms }} ms lower
            test_time=${{ steps.test.outputs.ms }} ms lower
            test_count=${{ steps.test.outputs.count }} higher
          max-regression: 25

For a whole job, record the start in the first step and the end in the last. Test frameworks that write JUnit XML give both the duration and the count: xmllint --xpath 'string(/testsuites/@time)' and string(/testsuites/@tests). Build systems with their own timers, like Gradle's build scan, Bazel's profile or cargo build --timings, can be read the same way; what reaches buildstats is a number with a unit of up to 16 characters.

Runner noise and the gate

Hosted runners vary, and a cold cache makes the first build after a dependency bump slower for reasons that have nothing to do with the code. The chart shows every point, so the noise band is visible, and a real change looks like a step. Set max-regression wider than the band: 25% is a reasonable start on hosted runners for a build that takes a minute or more, and much tighter on a self-hosted runner. A metric without a base value yet never fails the step.

Timing a job across operating systems works with variant: ${{ matrix.os }}, which stores test_time/ubuntu-latest and test_time/macos-latest as separate lines on one chart.

The chart in the README

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://buildstats.io/acme/app/test_time.svg?theme=dark">
  <img alt="test suite time per commit" src="https://buildstats.io/acme/app/test_time.svg">
</picture>

The SVG shows the last 200 points by default; n goes from 30 to 1,000 and w from 400 to 1,200 pixels. The badge at /test_time/badge.svg shows the latest value in the shields.io flat style. Public images are cached for 5 minutes.

Limits and pricing

Public repositoriesPrivate repositories
PriceFree$9/month per GitHub user or organization
TrialNot needed14 days, no card
Metrics per project100100
Points per metric and branch50,00050,000
Metrics per push5050
Pushes per minute and project6060
MembersUnlimitedUnlimited
Pull request refsKept 90 days after the last pushKept 90 days after the last push

Full details are on the pricing page.

Other ways to do this

GitHub Actions Performance Metrics, under the Insights tab, show average run time, queue time and failure rate per workflow and job with a custom range of up to 100 days, without a per-commit view or a pull request comment. Step timings also appear on each run page and vanish with the run's retention. Services like BuildPulse and Trunk focus on flaky tests and test analytics. For a per-commit chart of any timing in the README and a delta on the pull request, pushing the number to buildstats is one step and no account.

Questions

What should I time?

The things that hurt when they get slower: the full compile, the test suite, the Docker build, the end-to-end run. Each is one metric; a project holds 100 of them.

Can I track the number of tests too?

Yes. Push test_count with the higher direction and it gets its own chart; the comment shows when a pull request removes tests.

Does buildstats read the job duration by itself?

No. Pushing happens inside the job, so the job's own total is not known yet. Time the steps you care about with date, or push the previous run's duration from a workflow_run workflow using the GitHub API.

How do I fail a pull request that makes CI slower?

Set max-regression on the step and require the check in branch protection. The step fails after writing the comment, so the numbers are on the failed check.

Can I push from other CI systems?

Yes. Create a project API key and POST the metrics with curl from GitLab CI, Jenkins, Buildkite or a cron job.

Last updated 2026-10-07.