Setting Per-Route Performance Budgets

A site-wide budget is either too loose for the landing page or too tight for the account dashboard, so it gets ignored on both. A per-route budget is specific enough to act on and few enough to maintain — provided each number comes from data rather than from the room. This guide sits under Performance Budgets & SLOs and covers choosing which routes get a budget, three defensible derivations, converting a field target into a lab-testable ceiling, and getting the policy agreed before the first breach.

Prerequisites

How to set them

Step 1 — Choose the routes worth budgeting

Rank templates by traffic multiplied by commercial weight, and take the top three to six. Budgets on forty routes are ignored on all forty.

-- Candidate ranking: volume, current standing, and the gap to the threshold.
SELECT
  route,
  count()                                       AS sessions,
  quantileTDigest(0.75)(value)                  AS p75,
  countIf(value > 2500) / count()               AS poor_ish_share,
  count() * greatest(0, quantileTDigest(0.75)(value) - 2500) AS opportunity
FROM rum_events
WHERE metric = 'LCP' AND ts >= now() - INTERVAL 28 DAY
GROUP BY route
HAVING sessions >= 5000
ORDER BY opportunity DESC
LIMIT 10;

The opportunity column — sessions multiplied by the gap to the threshold — is the ranking that matters. The worst route is rarely the most valuable one to fix, because it is rarely the busiest.

Worst route against most valuable routeThe account route has the worst p75 and the least opportunity. Ranking by badness alone would have put the wrong route first.Worst route against most valuable routeSessions multiplied by the gap to the Good threshold, 28 days/products/[slug]428k/search186k/ (home)94k/account/orders31k0k125k250k375k500k
The account route has the worst p75 and the least opportunity. Ranking by badness alone would have put the wrong route first.

Step 2 — Pick a derivation and write it down

Three derivations hold up under questioning. Choose per route rather than per site.

From the assessment. The route needs to be Good at p75, so the target is the threshold. Simple, defensible, and appropriate for any route search traffic reaches.

From the conversion curve. Where you have the bucketed data from Conversion Funnel Correlation, set the budget where the curve bends. On many commerce templates the drop between the 2.0–2.5 s and 2.5–3.5 s buckets is far larger than the drops either side, which makes 2.5 s the right number for commercial reasons as well as specification ones.

From the competitive position. Where the peer benchmark in Querying the CrUX BigQuery Dataset puts you mid-table, a budget that would move you into the top third is a business objective with an obvious owner.

Route Derivation Field target Why this one
/ Assessment LCP 2.2 s High volume, search entry, needs headroom
/products/[slug] Conversion curve LCP 2.5 s Curve bends here; directly monetised
/search Assessment INP 200 ms Interaction-heavy; LCP is not the constraint
/account/* Competitive LCP 3.5 s Authenticated, low volume, not assessed

Step 3 — Set the number achievable within a quarter

A budget 10–25% below today’s value is the band that works. Above 25% it is breached on the first deploy and ignored by the second; below 10% it is a description rather than a target.

// derive-budget.mjs — a rule, not a negotiation.
export function deriveBudget({ currentP75, threshold, maxImprovement = 0.25 }) {
  const floor = currentP75 * (1 - maxImprovement);   // most we ask for in a quarter
  const target = Math.max(threshold, floor);          // never below the threshold
  return {
    fieldTarget: Math.round(target),
    stretch: Math.round(threshold),
    reachable: target <= currentP75 * 0.9,            // is it actually a target?
  };
}

Where the threshold is more than 25% away, set the quarter’s budget at the achievable number and record the threshold as the stretch goal. A two-step budget is honest; a single unreachable one teaches everyone to ignore the dashboard.

Step 4 — Convert the field target into a lab ceiling

CI cannot measure the field, so the number a pull request is checked against has to be the lab equivalent, derived through the measured ratio.

// The ratio comes from the correlation report; never guess it.
export const labCeiling = (fieldTarget, labToFieldRatio) =>
  Math.round(fieldTarget * labToFieldRatio);

// e.g. field target 2500 ms, measured ratio 0.72 → lab ceiling 1800 ms

Alongside the metric ceiling, set resource budgets, which are what an engineer can actually act on inside a pull request.

Resource Typical budget Rationale
JavaScript, initial route 170 KB gzipped Roughly 350 ms parse and compile on a mid-tier device
Above-the-fold images 200 KB One correctly sized hero in a modern format
Total blocking time (4× CPU) 300 ms Correlates with field INP better than any single-page metric
Third-party requests before load 4 Each is an uncontrolled dependency

Step 5 — Store all of it in one machine-readable place

Budgets drift the moment they exist in three places. One file, imported by CI, the dashboard and the review.

// budgets.config.js — the single source of truth for every consumer.
export const budgets = {
  '/products/[slug]': {
    owner: 'commerce-web',
    derivation: 'conversion-curve, 2026-07',
    field: { metric: 'LCP', target: 2500, cohort: 'mid' },
    lab: { lcp: 1800, tbt: 300, jsBytes: 190_000 },
    ratio: { lcp: 0.72, measuredAt: '2026-07-14' },
    policy: 'freeze-on-exhaustion',
  },
};

The derivation and ratio.measuredAt fields are what make the number survive a re-org. A budget with a recorded origin can be re-derived; a bare number gets overruled the first time it blocks a launch.

Where each route stands against its budgetThree of four over budget. Combined with the opportunity ranking, the product route is unambiguously first.Where each route stands against its budgetField p75 against the agreed target, 28 days/ (target 2200)1,920 ms/products (target 2500)2,880 ms/search (target 2800)3,240 ms/account (target 3500)3,760 ms0 ms1,250 ms2,500 ms3,750 ms5,000 ms
Three of four over budget. Combined with the opportunity ranking, the product route is unambiguously first.

Step 6 — Agree the policy before the first breach

A budget with no consequence is a chart. Agree, in advance and in writing, what happens when it is exceeded: a mandatory item in the next sprint, a freeze on new work for that surface, or an explicit renegotiation with a date and a reason. Agreeing this during a breach is a negotiation nobody wins.

Keeping budgets alive

Budgets decay in three predictable ways, and each has a cheap countermeasure.

The derivation goes stale. A budget set from last year’s conversion curve describes a page that has changed. Re-derive quarterly; it takes an hour and it is the step that keeps the number credible.

The ratio drifts. The lab ceiling is only correct while the lab-to-field ratio holds. When the correlation report shows the ratio moving, the CI budget is wrong and will either block everything or catch nothing.

Ownership evaporates. A budget whose owner changed teams is enforced by nobody. Put the owner in the config file, and check it during the quarterly review.

The fourth failure — a budget permanently breached and permanently ignored — is the one to watch for, because it is contagious. If a route has been over budget for two quarters, either the budget was wrong or the work is not going to happen. Both are decisions; leaving it red is not.

Verifying it works

  1. Every budgeted route has a derivation recorded, with a date.
  2. The lab ceiling is derived from a measured ratio, not chosen.
  3. At least one budget is currently being met and one is not. All green means they are too loose; all red means they are unreachable.
  4. CI blocks on the resource budget and does not block on the field SLO, which lags by design.
  5. The quarterly re-derivation actually happens. Put it in a calendar; it is the step everyone skips.
Deriving a budget from the conversion curveThe drop between 2.5 s and 3.0 s is nearly three times the drop before it. That bend is the number to budget at, and it happens to coincide with the specification threshold.Deriving a budget from the conversion curveProduct template, conversion rate by LCP bucket midpoint0%2.5%5%7.5%10%0 s2.5 s5 s7.5 s10 sLCP bucket midpoint (s)Conversion ratecurve bends near 2.5 s
The drop between 2.5 s and 3.0 s is nearly three times the drop before it. That bend is the number to budget at, and it happens to coincide with the specification threshold.

Edge cases and gotchas

  • Budgets derived from desktop data. A budget met permanently on desktop and never on mobile teaches nothing. Derive from the cohort that is failing.
  • Too many budgeted routes. Six is a lot; ten is a spreadsheet nobody reads.
  • Low-traffic routes. Below a few thousand sessions a window, the p75 is noise. Group them and budget the group.
  • Budgets set without the owner in the room. They get overruled the first time they block a launch, and the programme loses credibility.
  • Ignoring the stretch goal. Recording the threshold as a stretch target keeps the direction honest when the quarter’s budget is above it.
  • Changing a budget silently. A number that moves without a recorded reason is indistinguishable from a number nobody believes.

FAQ

How many routes should have a budget?

Three to six templates covering most traffic and revenue. Every extra one is another number to re-derive quarterly and another line in a review nobody has time for.

Should the budget be on LCP or on bytes?

Both, and they serve different purposes. The metric budget is the objective; the resource budget is what an engineer can check inside a pull request before the field data exists.

What if the threshold is unreachable this quarter?

Set the quarter’s budget at the achievable number and record the threshold as a stretch goal. A two-step target is credible; a single unreachable one is ignored.

Who signs off a budget?

The team that owns the route, with the product owner agreeing the target and the policy. A budget written by a platform team alone is advice.