ObservabilityGENERALPLATFORM-SPECIFICSIMULATED

Release Health

Attributing a regression to a deploy when clients update on their own schedule: tagging every signal with a release, reading the adoption curve, and comparing cohorts instead of time windows.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Errors went up an hour after we deployed — was it the deploy, and how would I know?

The user intent

A team ships a change and wants one thing: to know quickly whether they made things worse for the people already using it, and to be able to stop if they did.

The obvious build

Deploy, then watch the error dashboard. If the line goes up in the next hour, roll back. If it does not, the release is fine.

Why it breaks

A web client does not update when you deploy. It updates when the person next loads the page — which for an open tab may be tomorrow, or never (Long-Lived Clients and Version Skew).

How it breaks in a real browser
  • A web client does not update when you deploy. It updates when the person next loads the page — which for an open tab may be tomorrow, or never (Long-Lived Clients and Version Skew).
  • So the hour after a deploy contains a mixture of old and new clients, and the mixture is changing continuously. Any number computed over that window is a weighted average of two different releases with weights you did not choose.
  • The traffic mix changes on its own. An hour later is a different hour: a different country is awake, a different device mix, a different ratio of new to returning users. That alone moves error and latency numbers without any code changing.
  • Raw counts move with traffic. A release that goes out at the start of a busy period produces more errors of every kind, including the ones it did not cause.
  • A regression that only affects a small segment is invisible in an aggregate for days, and by then several more releases have shipped on top of it.
  • The rollback is also a deploy, and the clients running the bad version do not vanish when you roll back — they persist until each one reloads.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Every signal a client emits must carry the release identifier of the code that emitted it: errors, field vitals, request outcomes, product events. Without that field, attribution is guesswork over a moving mixture.
  • The identifier has to be injected at build time and tied to the artefact — the same content hash the bundle is served under — so that a stack trace, a source map and a metric all agree on which build they belong to (Content-Hashed Assets).
  • The adoption curve is the share of active sessions on each release over time. It starts at zero, rises as people load the page, and has a long tail of clients that stay on an old version for as long as their tab stays open. It is the shape that makes cohort comparison necessary.
  • A cohort comparison computes the same rate separately for each release, over each release's own sessions, rather than over a time window. Release A had this crash-free rate across its sessions; release B has this one across its sessions. The comparison is then like-for-like even though the two overlap in time.
  • The rates that matter are normalised: crash-free sessions, crash-free users, failure rate per request, and a field-vitals distribution per release. Counts are traffic; rates are quality (Release Health is about the second).
  • Statistical confidence arrives with adoption. Early in a rollout the new cohort is small and skewed toward whoever reloads first — often the most active users on the fastest connections — so the first readings are both noisy and unrepresentative.
  • A staged rollout gives you a controlled cohort before full adoption, whether through a served-asset split or a client-side flag, and converts "watch and hope" into a comparison with a control group (Feature Flags in the Client).

What this makes the browser do

And which of it is avoidable.

  • The release identifier costs one short string per beacon. It is the cheapest field in telemetry and the one most often missing.
  • A new deploy invalidates cached bundles by content hash, so early adopters pay a cold-cache load. Some of the loading regression visible right after a release is the cache, not the code (Browser HTTP Caching).
  • A service worker adds a second update schedule on top of the browser cache, and a client can hold an old version well beyond what the HTTP cache alone would explain (The Service Worker Lifecycle).
  • Prompting an open tab to reload for a new release is a foreground action with a cost: reload work, lost in-progress state, and an interruption. It is the mechanism that shortens the tail, and it is not free.

Tag everything, once, at build time

Everything downstream depends on one string being present on every signal. The identifier should be the same value the artefact is served under, so that a metric, a stack trace and a source map can be joined without anyone maintaining a mapping.

The failure this prevents is specific and common: an incident in which the first ten minutes are spent establishing which clients were running which code, because the answer is not in the data.

One constant, attached to every signal
1// Injected by the build. Same value as the asset content hash, so telemetry,
2// source maps and deploy markers all name the identical artefact.
3declare const __RELEASE__: string
4
5export const RELEASE = __RELEASE__
6
7// Attached at the single point every signal passes through — not at each
8// call site, where one surface will eventually be missed.
9export function envelope<T extends object>(signal: T) {
10 return {
11 ...signal,
12 release: RELEASE,
13 // occurredAt, not receivedAt: a beacon from a session that spans a deploy
14 // can arrive long after the release it belongs to stopped being current.
15 occurredAt: Date.now(),
16 sessionStartedOnRelease: SESSION_START_RELEASE,
17 }
18}
19
20// The same value goes to the source-map upload and the deploy marker:
21// npx upload-sourcemaps --release "$RELEASE" ./dist
22// curl -X POST /deploys -d "{\"release\":\"$RELEASE\"}"

sessionStartedOnRelease is the field people leave out. A session that begins on one release and continues past a deploy is not cleanly a member of either cohort, and recording both lets the analysis exclude it rather than silently miscount it.

Clients do not update when you deploy

SIMULATEDThese spans are a teaching model rather than a measurement — the relative ordering and the length of the tail are what transfer, and a product with short cold-load visits will have a far steeper curve than one people keep open all day, so plot your own from active sessions per release.

The adoption curve is the reason time-window comparison fails. At the moment of deploy, essentially nobody is running the new release. An hour later some fraction is. The tail — tabs left open, service workers holding a version, users who have not returned — can persist far longer than anyone's intuition suggests.

Two practical consequences fall out of the shape. First, any metric measured over the window immediately after a deploy is mostly measuring the old release. Second, a rollback is not instantaneous: the clients running the bad bundle continue to run it until each one reloads, so the shape of your own curve tells you how long a rollback actually takes to take effect.

Adoption after a deploy, schematicrelative units — ordering and shape, not durations
Deploy completes; assets swap
Release N-1 still dominant
Fast reloaders adopt N
Bulk adoption
Long tail on N-1 and older
Regression detected in cohort N
Rollback deployed
Clients still running N
  • Deploy completes; assets swapServer-side this is the event. Client-side almost nothing has happened yet.
  • Release N-1 still dominantEvery metric measured in this stretch is mostly a measurement of the previous release.
  • Fast reloaders adopt NNot a random sample: heavy users, fast connections, people who navigate often.
  • Bulk adoptionOnly here does the new cohort get large enough for its rates to mean much.
  • Long tail on N-1 and olderOpen tabs, cached service workers, infrequent visitors. Old releases keep reporting.
  • Regression detected in cohort NDetected by comparing N's own sessions against N-1's, not by a line moving on a clock.
  • Clients still running NThe rollback is a new deploy with its own adoption curve. The bad release does not disappear; it decays.

The last row is the one that surprises people. Rolling back changes what new loads receive; it does nothing for a client that already has the bundle.

Compare cohorts, not clocks

The final move is to change what you compare. A time-window comparison asks "what happened before and after"; a cohort comparison asks "what happens to sessions running this build". The second question is answerable and the first is not, because between the two windows the release mixture, the traffic mix and the device mix all changed.

A staged rollout strengthens this further by giving you a control cohort running at the same time, which holds traffic mix constant. Without one, cohort comparison still beats a time window, but it does not fully control for who happened to reload early.

Two readings of the same deploy
Time-window comparison
errors/min, hour before deploy vs hour after:

  before:  steady
  after:   up sharply

=> "the deploy broke it, roll back"

unexamined: what fraction of that hour was even
running the new build; what fraction of the rise
is the daily traffic peak; whether it is one route,
one browser, or everything.
Cohort comparison
crash-free sessions, per release, over each release's own sessions:

  release       sessions   crash-free   adoption
  2026.08.2     large      baseline     declining
  2026.08.3     growing    baseline     rising

  segmented — 2026.08.3 only:
    desktop            baseline
    mobile, engine A   baseline
    mobile, engine B   clearly worse   <- here

=> not a rollback: one engine, one route.
   Ship a targeted fix; leave the release up.

The cohort view computes each rate over the sessions that actually ran that build, so the comparison is unaffected by how fast adoption happened or by what the traffic mix did in the meantime. It also produced a better decision than a rollback — a targeted fix — which the aggregate view had no way to reach because it never separated the populations.

How to build it

Most important first.

  • Inject the release identifier at build time as a constant and attach it to every outbound signal without exception. One missing surface makes the whole picture unreliable.
  • Use the same identifier everywhere — telemetry, source-map upload, deploy markers, feature-flag targeting — so that a spike, a stack trace and a deploy can be joined without a translation table ("What Changed?" — Deploy Markers and the Invisible Deploys in Observability & Performance).
  • Publish the adoption curve next to every release metric. A rate from a release at low adoption is not comparable to one at full adoption, and showing them side by side prevents the comparison being made accidentally.
  • Compare cohorts, not windows. Ask "what is release B's crash-free session rate across its own sessions" rather than "did errors rise this afternoon".
  • Watch rates, not counts, and segment by device class, route and region before concluding anything (Real User Monitoring).
  • Roll out in stages so there is a control cohort running concurrently — the only comparison that holds traffic mix constant (Deploying a Frontend).
  • Define the abort condition before shipping: which rate, which segment, which threshold relative to the control cohort, and who decides. Deciding during an incident produces a debate rather than a rollback.
  • Give long-lived clients a path forward: detect that a newer release exists and offer a reload at a moment that does not destroy work in progress (Long-Lived Clients and Version Skew).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • An update prompt is an interruption, and it must follow the rules for one: reachable and dismissable by keyboard, announced rather than merely appearing, and not stealing focus from a field someone is typing into (Focus Management).
  • Do not force-reload without warning. A reload discards form state and returns a screen-reader user to the top of a document they had navigated deep into, with no explanation of why (Live Regions and Announcement).
  • Accessibility regressions do not appear in release-health metrics at all. A release that removes a label, breaks a focus trap or drops a live region produces no error, no failed request and no change in any vital — so accessibility must be gated before the release rather than watched after it (Accessibility Testing).
  • Segment field metrics by input modality where you collect it. A responsiveness regression that only affects keyboard-driven flows will otherwise be averaged away by a pointer-using majority.

What can go wrong

Failure modes
  • Signals without a release tag, discovered during an incident, when it is far too late to add one.
  • Rolling back the server assets and believing the problem is over, while every client that already loaded the bad bundle keeps running it.
  • Comparing the hour before and the hour after a deploy, and attributing to code a change that was really the daily traffic cycle.
  • Reading a rate at low adoption and treating the early cohort as representative, when it is composed of the users who reload most often.
  • A regression confined to one browser, one device class or one route, averaged into invisibility by a global rate.
  • Two releases in flight at once — a flag rollout on top of a deploy — so nothing can be attributed to either (Feature Flags in the Client).
  • A build that changes the release identifier without changing the source-map upload, so the new cohort's stacks are unsymbolicated exactly when you need to read them (Source Maps).
  • Prompting every open tab to reload the moment a release lands, destroying half-completed forms across your user base in the name of a clean adoption curve.
What can arrive out of order
  • A session that begins before a deploy and continues after it emits signals from the old bundle with the old release tag, arriving after the new release is live. Event time and release tag must both be recorded; ingest time attributes it to the wrong cohort.
  • A client can load the new HTML and a cached old bundle, or the reverse, during the window in which a deploy is propagating through caches and CDN nodes — producing a client that matches no release cleanly (Deploying a Frontend).
  • Two releases can be in the adoption curve simultaneously while a third is deploying, which is normal and makes "the current version" an ill-defined phrase.
  • A flag evaluated at load can differ from the flag state a moment later, so two clients on the same release can behave differently and report under the same tag.
  • Beacons from a rolled-back release keep arriving after the rollback, and a dashboard that attributes by arrival time shows the fixed release getting worse.
Security
  • A release identifier is metadata about your deployment, and it is already public — it is in the asset URL. Tagging telemetry with it discloses nothing new.
  • Cohort analysis works on rates and does not require identifying individuals. Building it on a stable cross-visit user identifier is a much larger data commitment than the analysis needs; session-scoped identifiers are usually enough (Analytics Events That Answer a Question).
  • Old clients are a security surface as well as a support surface. A release that fixed a client-side vulnerability is not deployed until the adoption curve says so, and the tail is the part that still has the bug (Long-Lived Clients and Version Skew).
  • A rollback must not silently re-enable a removed client-side check. The client is not an authorization boundary in either direction, and a rolled-back bundle is an old bundle with old assumptions (What the Frontend Is Responsible For in Auth).
  • Deploy metadata in telemetry should not extend to build environment details, internal hostnames or CI paths, which have a habit of appearing in stack frames and bundled paths.
Misreads
  • "We rolled back, so the bad version is gone." The assets are gone. Every client that already loaded the bad bundle is still running it, and will be until it reloads.
  • "Errors rose after the deploy, so the deploy caused it." The deploy also coincides with a changing traffic mix and a changing release mixture. Cohorts settle it; adjacency does not.
  • "The new release looks worse." Early adopters are not a random sample — they are whoever reloads first, which skews toward heavy users and fast connections. Read the adoption curve before reading the rate.
  • "Error counts are down, so the release is healthy." Counts follow traffic. Rates follow quality, and only rates can be compared between cohorts of different sizes.
  • "A release is one thing." A deploy plus an in-flight flag rollout is at least two, and attributing a change to either requires them not to move together (Feature Flags in the Client).
  • "The version in the URL is enough." It is, only if the same identifier is on the telemetry, on the source maps and on the deploy marker. Three names for one build is the same as no name.

Measuring it, and what changes in the field

How you would see this
  • Crash-free sessions and crash-free users per release, over each release's own sessions — the headline release-health rate.
  • The adoption curve: active sessions per release over time, including how long the tail of old releases persists. That number tells you how long a rollback actually takes to be effective.
  • Client request failure rate per release, which catches regressions that produce no exception at all (Network Failures Only the Client Can See).
  • Field vitals distributions per release, segmented by device class, so a responsiveness regression is separated from a device-mix change (Vitals in the Field).
  • A staged-rollout comparison: the new cohort against the concurrent control cohort, which is the only comparison immune to traffic-mix shift (Canary: Let 5% of Traffic Find the Bug in Cloud & Infrastructure).
  • Deploy markers overlaid on every chart, so the eye can associate a change with an event without anyone having to remember the time ("What Changed?" — Deploy Markers and the Invisible Deploys in Observability & Performance).
Slow device, slow network, large data, old tab
  • In a product people keep open all day — a dashboard, an inbox, an editor — the adoption tail is long and old releases can dominate for hours after a deploy.
  • In a product with short visits and cold loads, adoption is nearly immediate and time-window comparison is much less wrong, which is why teams from that world are surprised by the problem when they meet it.
  • With a service worker in play, clients can hold a version well past its cache lifetime, and the update strategy becomes part of the release story rather than a detail (The Service Worker Lifecycle).
  • On a slow network, adoption lags further because a large new bundle takes longer to fetch on first load after the change.
  • During an incident, adoption is what makes a rollback effective. Knowing the shape of your own curve in advance means knowing whether rollback is a minute-scale or an hour-scale mitigation.
What this costs
  • Tagging everything with a release adds a dimension to every metric. That multiplies series count and cost in your telemetry store, and it is the reason the field sometimes gets dropped (Cardinality: The Label That Took Down Monitoring in Observability & Performance).
  • Staged rollouts give you a control cohort and mean two versions of the client are live at once, which the API must tolerate and the team must reason about (How API Shape Drives UI Complexity).
  • Waiting for adoption before judging a release makes the judgement sound and slows the feedback loop the team wanted from continuous deployment.
  • Shortening the adoption tail with update prompts improves attribution and interrupts people mid-task. The gentler the prompt, the longer the tail.
  • Cohort comparison is more machinery than a line on a chart, and it is the difference between an argument about causation and an answer.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThat web clients do not update atomically follows from how the platform works — a loaded document keeps running its bundle until it is reloaded — and holds in every browser. It is not a property of any framework, bundler or deployment tool, and no build-time configuration can change it.
  • PLATFORM-SPECIFICNative mobile and desktop applications have a different adoption problem with the same shape but a much longer tail measured in weeks and gated by an app store, whereas a browser client can in principle update on the next navigation — so release-health practice imported from a mobile team assumes a slower curve than the web actually has.
  • SIMULATEDThe adoption timeline in this lesson is a schematic shape produced for teaching, not a measurement of any real deployment; the relative ordering — a fast initial rise, a long tail, old releases persisting well past rollback — is what transfers, and every real product should plot its own curve rather than assume this one.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Distributed Systems — a population of clients running several versions of your code against one API is version skew, and the compatibility rules that make it survivable are the same ones a rolling upgrade needs.