Setting Build Time Budgets for GitHub Actions

A build time budget is a p50 and p95 target per workflow class. Set the targets from measured percentiles, enforce a ceiling with timeouts, price the fix.

A build time budget is a stated p50 and p95 target for one class of workflows, checked against measured percentiles on a fixed cadence, with a named action when a target breaks. Write one budget per class rather than a single number for the whole estate, because the developer watching a pull request gate and the release captain reading a nightly report tolerate very different durations.

This guide covers the workflow classes worth budgeting separately, a budget table with p50 and p95 targets sitting beside the current numbers, where those numbers come from in WarpBuild Reports, the escalation rule that fires when a budget breaks, and the arithmetic on buying the time back with a larger runner size.

Diagnosis

A build time budget is missing when nobody in the room can say whether 14 minutes is acceptable for a given workflow. Three symptoms follow from that gap.

No verdict. A duration is reported, someone frowns, and the conversation moves on. Without a target the number carries no decision, so it produces discussion instead of work.

Regressions arrive as complaints. The first signal that a gate got slower is a developer in Slack, which means the regression has already been shipping for days. A budget converts the same event into a threshold crossing on a weekly reading.

The budget sits on the mean. A mean over a few hundred runs moves slowly and hides the runs that hurt. A gate whose slowest tenth doubled can hold a flat average for a month.

Budget by class, because the tolerance comes from what the person is doing while the job runs. Five classes cover most estates.

Workflow classWhat the person is doingTarget p50Target p95Current p50Current p95Verdict
Pull request gate, lint and unitWatching the checks tab5.0 min8.0 min4.2 min7.4 minInside
Pull request gate, integrationContext switching and back10.0 min15.0 min11.8 min24.0 minp50 and p95 broken
Merge queueHolding up other merges12.0 min18.0 min10.5 min16.2 minInside
Release build and publishRelease captain waiting20.0 min30.0 min22.4 min29.1 minp50 broken
Nightly full suiteAsleep60.0 min90.0 min52.0 min141.0 minp95 broken

Read the spread as well as the two figures. The ratio of p95 to p50 says which kind of problem the class has. The integration gate above sits at 24.0 over 11.8, a ratio above two, which describes a tail rather than a uniformly slow job. The nightly suite is worse at 141.0 over 52.0, and a nightly job with that shape is usually retrying something rather than computing something.

Queue time gets its own budget line and stays out of the duration budget. Waiting for a runner happens before the first step, no workflow edit changes it, and queued minutes are not billed, so folding it into the duration figure mixes two problems with two different owners.

Fix

The measurement source is the Jobs section of WarpBuild Reports, which aggregates per unique repository, workflow, and job name combination and reports run count, success rate, Duration P75 and P90, Queue Time P75 and P90, and CPU and memory percentiles where Observability is enabled. Filters cover repository, workflow, job name, runner label, and stack, and every tab exports CSV containing all rows matching the current filters and sort order.

That gives P75 and P90 without any work. Two paths follow from there.

Path one: budget on P75 and P90. The report hands you both, the time-series chart plots either percentile per job over time, and the numbers need no post-processing. Most teams should stop here.

Path two: compute p50 and p95 exactly. The CI billing tab is one row per job execution carrying repository, job name, runner label, stack, execution time, and billed time. Export it and any percentile is one sort away.

# p50 and p95 in minutes for one job, from the CI billing CSV export
# set COL to the execution time column index in your export
COL=6
awk -F, -v col="$COL" 'NR>1 && $2=="integration-tests" {print $col}' ci-billing-2026-08.csv \
  | sort -n > /tmp/durations

N=$(wc -l < /tmp/durations)
awk -v n="$N" 'NR==int((n+1)/2)   {print "p50", $1}' /tmp/durations
awk -v n="$N" 'NR==int((n*95+99)/100) {print "p95", $1}' /tmp/durations

Use execution time for the budget and billed time only when reconciling an invoice.

The loop that turns those numbers into a budget is five fixed steps.

  1. Assign every workflow to a class. A workflow with no class gets the gate class by default, which is the strictest, so unclassified work surfaces quickly.
  2. Baseline over 30 days with the filters and sort fixed, taken on the same weekday each time.
  3. Set the p50 target at or slightly below the measured p50, and the p95 target at about 1.5 times the p50 target.
  4. Publish the table where the team reviews it, with the current column refreshed weekly from the same export.
  5. Apply the escalation rule below, so a broken budget has an owner and a deadline instead of a frown.

The escalation rule

TriggerConditionActionDeadline
First reading over targetp95 over, p50 insideRead queue time P90 and success rate on the same row before touching the workflowSame week
Second consecutive readingStill overOpen an issue with both weekly exports attached and pick one lever: cache, sharding, or sizeOne week
Third consecutive readingStill overBuy the time back with the next size up and record the monthly delta from the model belowSame day
Any readingp50 and p95 both overTreat as a regression on every run and bisect against the last week that held48 hours

The order matters. Runner size is the last lever because it is the only one that raises the invoice, and CPU P90 is the test for whether it will work at all: a job pinned near the ceiling returns wall clock time for cores, and a job sitting in the low tens returns nothing. The Recommendations view flags an instance once max sustained CPU or max memory utilization reaches 80 percent, and Observability collects metrics only for jobs longer than about one minute. When the number that moved is queue time rather than duration, alerting on CI regressions covers the notification path.

Configuration

Percentile history is keyed on repository, workflow, and job name, so a renamed job starts an empty row and loses the baseline the budget was set from. Fix the job names first, then encode the class ceiling with timeout-minutes.

name: pr-gate
on:
  pull_request:

jobs:
  lint:
    runs-on: warp-ubuntu-latest-x64-2x
    timeout-minutes: 14
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run lint

  unit-tests:
    runs-on: warp-ubuntu-latest-x64-4x
    timeout-minutes: 14
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm test -- --shard=${{ matrix.shard }}/4

  integration-tests:
    needs: unit-tests
    runs-on: warp-ubuntu-latest-x64-8x
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@v4
      - run: make integration

Set timeout-minutes above the class p95 target with headroom rather than at it. A p95 target of 15.0 minutes means one run in twenty is expected to exceed it, so a timeout set at 15 converts normal tail runs into red checks. The 25 above catches a runaway run while leaving the tail alone, and the budget stays the thing that reports the tail.

Filtering the Jobs report by the warp-ubuntu-latest-x64-8x label then shows every job on that size across every repository, which is the view for deciding whether one size is carrying more classes than it should. A Windows or macOS class budgets from the same table with its own targets.

Cost or Time Model

Take the integration gate from the budget table, broken at p50 11.8 and p95 24.0 against targets of 10.0 and 15.0, running 900 times a month on warp-ubuntu-latest-x64-8x with CPU P90 near the ceiling. Linux x64 rates from the pricing page, billed per minute:

LabelvCPURAMStorageUSD per minute
warp-ubuntu-latest-x64-2x28 GB150GB SSD$0.004
warp-ubuntu-latest-x64-4x416 GB150GB SSD$0.008
warp-ubuntu-latest-x64-8x832 GB150GB SSD$0.016
warp-ubuntu-latest-x64-16x1664 GB150GB SSD$0.032
warp-ubuntu-latest-x64-32x32128 GB150GB SSD$0.064

Percentiles do not multiply into an invoice, so the bill needs a mean. Assume the mean duration is 13.0 minutes on the 8x size and 8.0 minutes after the move to 16x, with p50 landing at 7.5 and p95 at 14.5, both inside target.

The bill. Today: 900 x 13.0 = 11,700 minutes at $0.016, which is $187.20 a month. After: 900 x 8.0 = 7,200 minutes at $0.032, which is $230.40 a month. The class costs $43.20 a month more to hold inside its budget. Break-even is a mean of 6.5 minutes on the 16x size, below which the move lowers the invoice as well as the clock.

The time. The tail-only bound counts the slowest 5 percent, 45 runs, each 9.5 minutes shorter: 427.5 minutes, or 7.1 hours a month. The whole-distribution bound uses the p50 move of 4.3 minutes across all 900 runs: 3,870 minutes, or 64.5 hours a month. The real figure sits between 7.1 and 64.5 hours, and four weeks of readings settle it better than any argument about the model.

Two structural notes for anyone turning a budget into a line item. Pricing is purely usage based. There is no base subscription fee, no platform fee, and no seat fee, so a size change lands on the invoice as exactly the minute arithmetic above. Signup includes $10 free credits, which covers a first pass at the model on one real workflow.

For the estate-wide view that these budgets roll up into, start at GitHub Actions observability with WarpBuild. For the percentile mechanics behind the targets, read tracking build duration percentiles, and when a budget breaks without an obvious cause, find which workflow got slower this month ranks the candidates.

FAQ

What should a build time budget be set to?

To a figure derived from the measured percentiles of the class over a fixed window, rounded to something a team can defend, rather than to a wish. Take a 30 day baseline, set the p50 target at or slightly below the current p50, and set the p95 target at about 1.5 times the p50 target so the budget carries a stated tolerance for the tail instead of pretending the tail does not exist.

Should the budget sit on p50 or p95?

On both, because they fail for different reasons. A broken p50 means every run got heavier and the lever is the job or the runner size. A broken p95 with p50 holding means a tail formed on a minority of runs, and queue time and success rate on the same row separate contention from retries.

Does the WarpBuild Jobs report give p50 and p95 directly?

The Jobs report carries Duration P75 and P90 and Queue Time P75 and P90 per unique repository, workflow, and job name, so P75 and P90 are the pair available without extra work. For p50 and p95 exactly, export the CI billing tab, which is one row per job execution with execution time, and compute the percentiles from the raw list.

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.