---
name: running-benchmarks
description: Use when a backpressured loop needs to run benchmarks on a performance-sensitive project and decide whether a change is a regression, an improvement, or a wash — per-iteration sanity checks and the full pre-done run.
---

# Running Benchmarks

## Overview

**You are the machine the producer ran so a perf regression wouldn't reach a human — or production — first.** Benchmarks are backpressure: they say "no" to a slow patch at the boundary where it was introduced, not three releases later when someone files a latency bug.

**Core principle: a benchmark number is meaningless on its own.** A single "after" run proves nothing. A verdict requires a *baseline to compare against* and a way to *separate a real change from run-to-run noise*. Everything below exists to enforce those two things, because the natural temptation is to run once and eyeball it — and that is worthless.

## When to Use

- A `backpressured` loop is on a **performance-sensitive** project and needs to confirm a patch didn't regress perf — a quick targeted check each iteration, and the **full** suite before leaving the loop.

**Not for:** projects with no benchmark suite or no perf sensitivity — skip benchmarking entirely rather than inventing it. This is not a correctness check; tests cover that.

## Step 1 — Discover the suites and their time budgets

If the project has a `BACKPRESSURE.md`, check it first — it may name the suite(s) and exact commands to use. Otherwise find the commands yourself — `package.json` scripts, `Makefile`, a `benchmarks/`/`bench/` dir, CI config, README. Identify which suites exist and roughly how long each takes. Expect a spread: fast micro/targeted suites (seconds) and full end-to-end suites (minutes). Note what each one actually measures so you can map a suite to the code path you touched.

## Step 2 — Pick the suite for the moment

- **Each iteration:** run the **fast, targeted** suite that covers the path you changed. It must be cheap enough to run after every patch without rabbit-holing.
- **Before done:** run the **full** suite once. Targeted-green is necessary, not sufficient — a change can help one path and regress another.

Don't run an 8-minute end-to-end suite to check a change that only touches one micro path; don't declare done off a micro suite alone.

## Step 3 — Always measure baseline vs patched

Never judge from the patched numbers alone. Run the **unchanged baseline** (git stash, or check out the parent commit / a worktree) and **your change** back-to-back, on the **same machine under the same conditions**. The baseline is the only thing that makes the "after" number mean anything.

Quiet the machine first — close background load, don't run benchmarks mid-build or while other heavy processes compete. Uncontrolled machine state is the largest real source of noise, and "same conditions" means *actually* the same, not just same hardware.

## Step 4 — Beat the noise with repetition

Benchmark output is noisy (often ±3–5% run to run). One run per side is not evidence.

- Run **each side multiple times** (≥5 when the suite is cheap).
- Compare the **median** (and the **min** for CPU-bound microbenchmarks — least contended, most stable), not a single sample.
- Establish the **noise band** from the spread of the repeated runs (or a known per-suite figure). The band is your threshold for everything below.

## Step 5 — The verdict: regression / improvement / wash

| Verdict | Condition | Action |
|---------|-----------|--------|
| **Wash** (neutral) | Patched median within the noise band of baseline; distributions overlap | **Passing.** Move on. |
| **Improvement** | Patched consistently faster, gap clears the noise band | **Passing.** Note the win. |
| **Regression** | Patched consistently slower, gap clears the noise band, distributions don't overlap | **FAIL.** Fix it, or stop and name the blocker — never wave it through. |
| **Ambiguous** | Result straddles the band (e.g. ~noise ± a little) | Not a verdict yet. Add runs (≥10/side) and/or run the full suite to resolve **before** deciding. |

If a suspicious result still won't separate from noise after more runs — a small true shift that the suite genuinely can't resolve — do **not** default it to a pass. Treat it as a blocker and name it (the change may need a less noisy benchmark, or a human call), exactly as you would any other unresolved check.

## Step 6 — Record the evidence

"No regression" asserted with no numbers is not a passing check. Record **baseline vs patched, run count, and the median/min** you compared. Evidence before assertion — the same standard the rest of the loop holds you to.

## Common rationalizations

| Rationalization | Reality |
|-----------------|---------|
| "I ran it once, the number looks fine" | One run inside a ±5% noise band tells you nothing. Baseline + repeats or it doesn't count. |
| "It's a tiny bit slower, probably noise" | "Probably noise" is a measurement you didn't take. If it straddles the band, add runs until it resolves. |
| "The micro suite is green, we're done" | Micro-green can hide a full-suite regression. Run the full suite before done. |
| "Running the baseline too is double the work" | Without a baseline the patched number is uninterpretable. Half the work producing nothing is not a saving. |
| "I'll just trust the full suite once at the end" | A regression found at the end is a bisect. Run the targeted suite each iteration. |

## Red flags — STOP

- Reporting a benchmark result with **no baseline** to compare against.
- Declaring "no regression" with **no recorded numbers**.
- Calling an **ambiguous** straddle-the-band result a pass instead of adding runs.
- Skipping the **full** suite before done because a targeted suite was green.
- Treating a real, beyond-noise slowdown as acceptable without fixing it or naming the blocker.
