---
title: "Canary rollouts"
description: "Ship a change to a percentage of installs instead of all-or-nothing."
---

Most flags in this repo are binary: a change is off, or on for everyone. A
canary is the rung in between — the same change, enabled for a stable slice of
installs, ramped as the signal holds.

<Note>
  A canary is a *measured* rollout: a slice is enrolled so it can be compared
  against everyone else. So **opting out of telemetry opts you out of
  canaries** — an opted-out install is never bucketed at all. See the
  [`telemetry` command](/packages/cli#telemetry) for the opt-out routes.
</Note>

## Add one

**1. Register it at 0%** in `packages/core/src/canaryRegistry.ts`:

```ts
{
  name: "my-feature",
  percentage: 0,
  description: "One line on what turning this on actually changes.",
  owner: "your-handle",
  sunsetAfter: "2026-12-01",
}
```

At `0` it is inert, so this lands safely on its own.

**2. Read it at the decision point.**

```ts
import { isCanaryEnabled } from "../telemetry/canary.js";

if (isCanaryEnabled("my-feature")) {
  // new path
} else {
  // existing path
}
```

That is the whole API. The percentage lives in the registry, never at the call
site.

**3. Ramp it** by editing `percentage` in a patch release: `0 → 5 → 25 → 100`.
Widening is inclusive — everyone already in the 10 stays in the 25, so a
before/after comparison survives the ramp. Do not rename a live canary: the
name is part of the bucket hash, so renaming reshuffles the cohort mid-rollout.

**4. Delete it** at 100 and holding — the registry entry *and* the branch it
guarded. The scheduled **Canary sunset** workflow enforces this: it runs weekly
and fails once `sunsetAfter` passes, naming the overdue rollout and its owner in
the run log. It notifies nobody, so watch it if you own a canary. Run it
yourself with `bun scripts/check-canary-sunset.ts`.

## Override one

```bash
HF_CANARY_MY_FEATURE=on   # or off / true / false / 1 / 0 / yes / no
```

Upper-snake-case the name. In Studio, use `?hf_canary_my_feature=on`. An
override always wins over the percentage, in both directions — escalations,
dogfooding, bisects, a panic-off. It is also the way to exercise a canary with
telemetry off.

Everything else fails closed below 100%: no bucket seed, an unregistered name,
or a CI machine all resolve to *not enrolled*. At exactly 100% those exclusions
stop applying, so nothing takes the new path for the first time at deletion.

## Remove one from your machine

Cohorts bucket on a seed in `install-state.json`, which sits beside
`config.json` in `~/.hyperframes` — not on the telemetry id. So a full reset is
one command:

```bash
rm -rf ~/.hyperframes    # clears telemetry id, canary cohorts, and breaker state
```

`hyperframes telemetry status` prints both paths if you want to inspect or
delete them individually. Nothing canary-related is stored anywhere else.

## Where it lives

| File | Role |
| --- | --- |
| `packages/core/src/canaryRegistry.ts` | Every rollout, with owner and sunset date |
| `packages/core/src/canary.ts` | Pure evaluator — no fs, no network, browser-safe |
| `packages/cli/src/telemetry/canary.ts` | CLI binding: bucket seed, env override, CI detection |
| `packages/studio/src/telemetry/canary.ts` | Studio binding: adopts the CLI's published decisions |

The evaluator is dependency-free so Studio and the embeddable player can use it
too; each surface supplies its own unit id through a thin binding. Studio
adopts the CLI's decisions rather than re-deriving them, so one render spanning
both surfaces never runs half-enrolled.

## Related topics

- [Testing local changes](/contributing/testing-local-changes) — exercise your
  canary before anyone else gets it.
- [Release channels](/contributing/release-channels) — which branch a ramp belongs on.
- [`telemetry` command](/packages/cli#telemetry) — what is collected, and how a
  user turns it off.
