Skip to main content

Canary releases for microfrontends

A canary release serves a new version of a microfrontend to a fraction of your traffic while everyone else keeps the stable one. MFE Orchestrator takes that decision per microfrontend and entirely server-side: the host page never learns which side of the split it got, and does not need to.

Enabling a canary

Open the microfrontend. Release is the first card of the form and it sits above the tabs, so it is on screen whichever of General, Hosting, Repository or Danger Zone is selected: the version and the canary decide which bytes a browser gets, and they are not something you should have to go looking for. Under the version, past a divider, is Canary Settings and its switch.

The Canary Settings section of the microfrontend form with the switch on, the percentage slider and its presets, the canary type and deployment type selects and the canary version field

Turned on, the section asks for four things, in this order:

FieldMeaning
Canary PercentageShare of traffic that should receive the canary, as a slider with 5, 10, 25 and 50 presets. Not asked for by User, which enrols people instead of splitting traffic
Canary TypeWho gets the canary — Random, Session or User
Deployment TypeWhat the canary is — Based on Version or Based on URL
Canary Version / Canary URLThe target, depending on the deployment type

A sentence under the two selects spells out what the selected type does, because the three names are not enough to tell them apart. The percentage has to be between 1 and 100 while the canary is enabled — 0 is refused, which is deliberate: a canary configured to reach nobody is a misconfiguration and not a way to switch it off. Turning the switch off is.

Canary configuration is part of the microfrontend, so like everything else it is captured in a deployment and only becomes live once you deploy. Expanding a deployment under Deployments shows a card per microfrontend with its stable version, the two types, and the canary share — or Enrolled users, for a User canary, which has no share. It is the quickest way to confirm what a deployment will actually do.

The microfrontend cards of an expanded deployment, each with its stable version, canary share, canary type and deployment type badges and canary version

The microfrontends list carries the same information in shorter form. A grid card shows the share as a bar with the canary version underneath; the table view gives it a column of its own, next to a Canary Version column, and says No canary for the microfrontends that have none.

A microfrontend card in grid view showing the canary block: the share as a percentage, a progress bar and the canary version

Who — the canary type

The three types answer two different questions, and it is worth being clear about which one you are asking.

The Canary Type select open, showing the three options Random, Session and User

Canary TypeWho gets the canarySticky?
RandomA share of every page load, drawn fresh each timeNo — the same browser flips between versions on every refresh
SessionA share of browsers, drawn once per browserYes — survives a reload, a new tab and a browser restart
UserOnly the users you enrol, no matter how many they areYes — it is a decision, not a draw

Random and Session are the same percentage split and differ only in whether the draw is remembered. User is not a split at all: the percentage plays no part in it.

The identity the split is computed on travels in the query string of the calls your host makes to the serve API, and the client SDK generates, persists and sends it for you:

Query parameterWhat it identifiesWhere the host keeps itUsed by
mfeDeviceIdThe browserlocalStorageSession
mfeUserIdThe logged-in userSupplied by your applicationUser
mfeSessionIdThe browsing sessionsessionStorageNo canary strategy — the SDK still sends it, and exposes it, for your own telemetry

The host sends every identity it holds and is never told which one was used, nor the percentage, nor whether a canary exists at all. Switching a microfrontend from one type to another is therefore a console change only — nothing to redeploy on the host side.

:::info No cookie is involved, and none can be An earlier version of this feature had a Cookie Based type, and the name was misleading: no cookie is involved anywhere here, and none can be. Microfrontends are loaded with a cross-site import(), and module scripts are fetched with a fixed same-origin credentials mode, so a cookie belonging to the MFE Orchestrator domain is never attached to those requests and a Set-Cookie on their responses is never stored. This is how module scripts are specified in every browser, not a Safari or ITP quirk — verified in Chromium with third-party cookies enabled and a SameSite=None; Secure cookie already in the jar. The host page is the only place that can hold this state, and the URL is the only way to hand it over.

That is why Session means "an identity that outlives the browser session", kept in localStorage by the SDK. If you would rather keep it in a first-party cookie of your own domain, read it yourself and pass it as mfeDeviceId.

The old On Sessions and Cookie Based types no longer exist. Microfrontends still configured with either of them — and the microfrontend snapshots inside deployments already shipped — are converted to Session automatically, by a migration the backend runs when it connects to the database. Nothing to do on your side. :::

What — the deployment type

TypeBehaviour
Based on VersionThe canary is a different version of the same microfrontend, served through its normal hosting configuration
Based on URLThe canary is a completely separate URL, which can point anywhere

Based on Version is the natural choice when the canary is simply the next build. Based on URL is useful when the candidate lives on separate infrastructure — a preview environment, a different CDN — and you want to send a slice of traffic there without registering it as a version.

How the split is computed

Random is what it says: a draw against the percentage, taken once per page load when the manifest is fetched, with nothing stored anywhere.

Session is sticky, and gets there without server-side state and without a draw per request. The identity and the microfrontend id are concatenated (identity:microfrontendId), hashed with FNV-1a into one of 100 buckets, and the request gets the canary when bucket < percentage.

Three properties follow from that, and they are the reason it is done this way:

  • Stable. The same identity always lands in the same bucket, so a returning browser stays on the side it was on, without anything being stored.
  • Monotonic. Raising the percentage only ever adds browsers to the canary; it never moves one back to stable. Measured going from 20% to 40%: zero browsers moved out.
  • Independent per microfrontend. The microfrontend id is part of the hashed string, so a browser can be on the canary of one microfrontend and on the stable version of another. A single unlucky user does not get every canary at once.

The distribution is accurate enough to trust the number you type: 25% configured measured 25.1% actual over 4000 identities.

When no identity arrives

  • Session falls back to a plain draw per page load, which is to say it degrades into Random. The page load itself stays coherent, because the resolved version is pinned into the URL immediately — but the next reload draws again. Stickiness is exactly what requires the host to send an identity, which is what the SDK is for.
  • User does not fall back. Without mfeUserId there is nobody to look up, so every request gets the stable version. That is the intended behaviour for anonymous visitors, not a degradation.

How the version reaches the browser

This is the subtle part, and worth understanding before you debug a rollout.

A canary that rolls out one of your versions does not change the URL of the microfrontend. The manifest returns the usual serve URL with the resolved version already pinned into it as a path segment:

https://console.mfe-orchestrator.dev/api/serve/mfe/files/auto/68f1…/checkout-new/_v/1.5.0-rc1/assets/remoteEntry.js

Use that URL verbatim. Never rebuild it, and never strip the _v/<version>/ segment.

A microfrontend is many files, and they all come from one build. The draw happens exactly once, on the entry point, and its result is pinned into that URL; every chunk imported from it inherits the _v/<version>/ segment, because relative specifiers resolve against the entry point's URL. The server never draws again for the individual files — a file arriving without a version in its path is served the deployed version, not a fresh coin flip. With a Random canary that distinction is the whole ball game: redrawing per file would let one page load half of build A and half of build B.

Why a path segment and not a query parameter. The entry point imports its chunks with relative specifiers (./chunk.js), which the browser resolves against the entry point's URL — and that resolution drops the query string. A version carried in the query would be lost by the first chunk request.

Why the URL is pinned up front rather than redirected to. The entry point of a canary microfrontend also answers the versionless URL with a 302 (uncacheable) to the pinned one, as a fallback for anyone holding a raw URL. That redirect is enough for an ES module, whose relative imports resolve against the post-redirect URL. It is not enough for a classic script: document.currentScript.src is the URL before redirects, so a webpack build with publicPath: 'auto' would compute its chunk base from the versionless URL and fetch chunks from a different version — two builds mixed into one page. Verified in Chromium: the classic script saw /mfe/assets/entry.js while an ES module saw /mfe/_v/1.5.0-rc1/assets/entry-esm.js. Pinning the URL in the manifest removes the redirect from the picture entirely, so both module systems resolve their chunks on one version.

A Based on URL canary is decided in the manifest instead: there is no version of yours to pin, so the host is simply handed either the canary URL or the stable one.

Verifying which version a browser got

ToolWhat it does
x-mfe-version response headerPresent on every file served for a microfrontend, naming the version those exact bytes came from
?mfeVersion=<version> query parameterForces a specific version, for testing

mfeVersion is accepted only when the value is one of the versions that deployment can serve — the deployed version or the configured canary version — so it cannot be used to reach an arbitrary build.

In practice: open the network panel, find the entry point request, and read x-mfe-version. That is the ground truth, and it is the only thing the browser is ever told about the rollout.

Canary users

User targeting is an enrolment list, not a split. A user sees the canary when the deployment carries a row enabling them, and everyone else — including every anonymous visitor — gets the stable version. Because the decision is taken on mfeUserId, it follows that person onto any browser and any device.

The list lives on the deployment and covers every microfrontend of it configured as User. Open a deployment under Deployments — the active one or any in the history — and press View canary users.

The canary users table with every row selected and the bulk enable, disable and remove actions

  • Add one or more user ids. Paste a list separated by commas, spaces or new lines — semicolons work too — to enrol a whole group in one go; the input de-duplicates, so a list copied out of a spreadsheet or a query result works as it is.
  • Toggle a row between Canary and Excluded to suspend someone without losing the entry.
  • Remove a row to send that person back to the stable version.
  • Select rows with the checkboxes to enable, disable or remove all of them at once — the same three actions, applied to the selection.

Removing a row and disabling it are different entries in the table and the same thing to the resolver: a user with no row and a user with a disabled row both get the stable version.

The ids are the ones your application knows — whatever it passes to the client SDK as userId. They are opaque strings to the console, which is why the page shows them back rather than names or email addresses.

Enrolment survives a new deployment

Enrolment is stored per deployment, which taken literally would mean that every deploy starts with nobody on the canary. It does not: creating a deployment copies the rows of the deployment the environment is serving right now into the new one, inside the transaction that creates it. Each row keeps its state, so someone you had disabled stays disabled rather than being re-enrolled.

A redeploy is a different operation — it re-activates a deployment that already holds its own rows, so there is nothing to carry over.

Telling the SDK who is logged in

The host is the only place that knows this, so it has to hand it over. Three shapes, depending on when the user becomes known:

// Known at bootstrap
configure({ backendUrl, projectId, userId: currentUser.id })

// Known after an auth round trip — the getter is resolved right before the manifest request
configure({ backendUrl, projectId, userId: () => authStore.getState().user?.id })

// Changing while the page is alive: a login, a logout, an account switch
import { setUserId } from '@mfe-orchestrator-hub/client'
auth.onLogin(user => setUserId(user.id))
auth.onLogout(() => setUserId(undefined))

setUserId() drops the memoised manifest, so the next remote resolved is decided on the new identity. What it cannot do is move a microfrontend that is already on the page: the federation runtime keeps the container it loaded, so an enrolment that arrives after the remote was imported takes effect on the next resolution or on a reload. Resolving your remotes behind your own auth guard avoids the question entirely.

Limits worth knowing

  • Based on URL canaries cannot be inspected the same way. The split itself works exactly as described — the type decides it, and Session is as sticky there as anywhere else — but there is no version of ours in play: the host is simply handed one URL or the other. Nothing serves those files, so they carry no x-mfe-version header and ?mfeVersion= has nothing to force. The manifest is no help either — with no version of ours to report, its version field names the microfrontend's stable version whichever of the two URLs it just handed out. Prefer Based on Version when you want to watch a rollout rather than just perform one.
  • The manifest is fetched once per page load and is not retried. If that request fails, the remotes of that page load fail with it; the SDK clears its memo so the next call tries again.

Practical advice

Pick the type before the percentage. Random is for exercising both builds against the same browser — a smoke test you can refresh, and a poor fit for anything a user is halfway through. Session is what a rollout normally means. User is for a named group: your own team, one customer, a support case.

Pick a percentage you can read. With a sticky split, the useful question is how many sessions you need before your dashboards say anything. 5% is a smoke test, 25% is a measurement, 50% is a decision you have already made. The presets on the slider exist for that reason.

Roll forward in steps, and lean on monotonicity. 5 → 10 → 25 → 50 → 100 is safe in a way that matters: every step keeps the browsers already on the canary where they are and only adds new ones, so you are never re-running the split on people who have been fine for an hour.

Roll back by turning the canary off, not by rewriting the version. The switch is the stop button: the fields keep what you typed, so the next attempt starts from the configuration you already had. Lowering the percentage is the gentler version of the same move, but it does not reach zero — the form refuses a canary that is enabled and set to 0%. For anything worse than a bad canary there is rollback and redeploy.

Verify with the header, not by counting. Before you trust a rollout, load the host twice — once in a normal window and once in a fresh private window — and read x-mfe-version on the entry point of each. Two different values is the split working. With a Random canary a plain refresh is enough. Use ?mfeVersion= when you need to see the canary on demand rather than by luck.

Promote by making the canary the version. Once you are convinced, set the microfrontend's version to what the canary was pointing at and turn the canary off. Nothing about the URLs your host holds changes.