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 class | What the person is doing | Target p50 | Target p95 | Current p50 | Current p95 | Verdict |
|---|---|---|---|---|---|---|
| Pull request gate, lint and unit | Watching the checks tab | 5.0 min | 8.0 min | 4.2 min | 7.4 min | Inside |
| Pull request gate, integration | Context switching and back | 10.0 min | 15.0 min | 11.8 min | 24.0 min | p50 and p95 broken |
| Merge queue | Holding up other merges | 12.0 min | 18.0 min | 10.5 min | 16.2 min | Inside |
| Release build and publish | Release captain waiting | 20.0 min | 30.0 min | 22.4 min | 29.1 min | p50 broken |
| Nightly full suite | Asleep | 60.0 min | 90.0 min | 52.0 min | 141.0 min | p95 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/durationsUse 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.
- 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.
- Baseline over 30 days with the filters and sort fixed, taken on the same weekday each time.
- Set the p50 target at or slightly below the measured p50, and the p95 target at about 1.5 times the p50 target.
- Publish the table where the team reviews it, with the current column refreshed weekly from the same export.
- Apply the escalation rule below, so a broken budget has an owner and a deadline instead of a frown.
The escalation rule
| Trigger | Condition | Action | Deadline |
|---|---|---|---|
| First reading over target | p95 over, p50 inside | Read queue time P90 and success rate on the same row before touching the workflow | Same week |
| Second consecutive reading | Still over | Open an issue with both weekly exports attached and pick one lever: cache, sharding, or size | One week |
| Third consecutive reading | Still over | Buy the time back with the next size up and record the monthly delta from the model below | Same day |
| Any reading | p50 and p95 both over | Treat as a regression on every run and bisect against the last week that held | 48 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 integrationSet 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:
| Label | vCPU | RAM | Storage | USD per minute |
|---|---|---|---|---|
warp-ubuntu-latest-x64-2x | 2 | 8 GB | 150GB SSD | $0.004 |
warp-ubuntu-latest-x64-4x | 4 | 16 GB | 150GB SSD | $0.008 |
warp-ubuntu-latest-x64-8x | 8 | 32 GB | 150GB SSD | $0.016 |
warp-ubuntu-latest-x64-16x | 16 | 64 GB | 150GB SSD | $0.032 |
warp-ubuntu-latest-x64-32x | 32 | 128 GB | 150GB 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.