Rolling Back a Bad Extension Release

Recover from a broken extension update — what each store can and cannot roll back, why a re-publish beats a rollback, and how to make the next version safe to downgrade into.

Published September 18, 2026 Updated September 18, 2026 8 min read
Table of Contents

A release goes out, error reports spike, and the instinct is to roll back. Extension stores mostly do not offer one. Chrome can stop a rollout and, in limited circumstances, revert to a previously published version; Firefox and Safari expect you to publish forward. Knowing which levers actually exist — before you need them — is the difference between a two-hour incident and a two-day one. This guide is part of extension updates and data migration.

What each store can actually do

Recovery levers by storeChrome Web Store, Firefox AMO and the App Store compared on halting a rollout, reverting to a previous version, unpublishing, and typical time to reach users.LeverChrome Web StoreFirefox AMOApp StoreHalt an in-progress rolloutYes, immediateNo staged rolloutPhased release: pauseRevert to a previous versionLimited, by support reque…Re-sign an old versionNoUnpublish the listingYes — installed copies st…YesRemove from salePublish a fix forwardHours to daysHours to weeksDaysUsers already updatedStay on the bad versionStayStay
Only the first row is fast everywhere — which is why a staged rollout is the lever that matters most.

The last row is the one that shapes everything else: nothing you do removes a bad version from a machine that already has it. Recovery is always about the next update reaching them, so the real question is how fast you can ship a good one.

Step-by-step

1. Stop the bleeding first

If the release is still rolling out, halt it. In the Chrome Web Store dashboard the percentage rollout control stops new users receiving the version immediately; on the App Store, pause the phased release.

This is the only lever that works in minutes, and it is available only if you used a staged rollout in the first place — which is the argument for making every release staged by default rather than only the ones you are nervous about.

2. Decide between fixing forward and reverting

Reverting sounds safer and usually is not. A revert is a new version with old code, so it goes through the same review queue as a fix, and it reintroduces whatever the release was meant to correct. Worse, if the bad version wrote a new data shape, the old code now meets data it does not understand.

1// The check that makes a revert survivable — present in the OLD version, or not at all.
2const { schema = 1 } = await chrome.storage.local.get("schema");
3if (schema > SCHEMA) {
4  await chrome.storage.local.set({ degraded: true });
5  console.warn("[migrate] newer data present; running read-only");
6}

Execution context: the service worker of the older build. This only helps if it shipped before the incident — which is the argument for writing every version to tolerate data from the future, as covered in running data migrations on onInstalled.

Fix forward when the bug is understood and the fix is small. Revert when the bug is not understood and the previous version is known good and forward-compatible.

3. Ship the smallest possible fix

An emergency release should change one thing. Reviewers are faster with a small diff, and a minimal change is one you can reason about at three in the morning.

1git checkout -b hotfix/2.4.1 v2.4.0
2git cherry-pick <the-one-fix>
3npm run build && npx web-ext lint --source-dir ./dist

Execution context: your shell. Branching from the released tag rather than from the main branch keeps unrelated work out of the hotfix — the most common way an emergency release introduces a second incident.

4. Use a kill switch where the failure allows it

Some failures can be turned off without a release at all, if the extension already reads a remote configuration. The feature has to have been built before the incident, and it must be config rather than code — remote code is prohibited.

1const { flags = {} } = await chrome.storage.local.get("flags");
2if (flags.newSyncEngine === false) return legacySync();

Execution context: the service worker, with flags refreshed from your endpoint on an alarm. The mechanics and the policy limits are set out in safely using remote config without remote code. A kill switch turns a store-review-bound incident into a deploy-bound one, which is usually an order of magnitude faster.

5. Tell users what happened

Users on the bad version are the last to know and the first to leave a review. An in-extension notice reaches them faster than a store listing update, and it costs one storage key.

1await chrome.storage.local.set({
2  notice: {
3    id: "2026-09-18-sync",
4    text: "Sync was failing in version 2.4.0. Updating to 2.4.1 fixes it.",
5    until: Date.now() + 7 * 864e5,
6  },
7});

Execution context: the service worker, written when the extension detects it is on an affected version. The popup renders it and the user dismisses it by id — a notice that reappears after dismissal is worse than none.

6. Write the post-incident change into the release process

Every one of these incidents produces a concrete change to how releases are made. Common outcomes, in rough order of value: make every release staged; add the schema-tolerance check to the migration; add a smoke test that loads the built artifact and exercises the main path; keep the previous artifact loadable for rehearsal.

An incident, from publish to resolutionA timeline showing a staged release, error detection, rollout halt, hotfix build, review and the long tail of users updating.publish+48 hRollout at 10%first usersE…r…R…m…Hotfix built …small diffReviewhours to daysUsers updateauto-update paceblast radius capped at 10%fix live
The halt takes minutes; everything after it is bounded by review and by auto-update pace.

Knowing within minutes, not days

Every lever above is only as useful as the speed at which you learn something is wrong. Store reviews are a lagging signal — by the time one-star reviews arrive, the rollout has usually reached most users. The leading signal is your own error reporting, bucketed by version.

 1// Attach the version to every report so a release can be compared with its predecessor.
 2const VERSION = chrome.runtime.getManifest().version;
 3
 4export function report(err, context) {
 5  return sendReport({
 6    version: VERSION,
 7    context,                      // "sw", "popup", "content"
 8    name: err.name,
 9    message: String(err.message).slice(0, 300),
10    at: Date.now(),
11  });
12}

Execution context: a shared module used by every context. Sending the version with each event is what makes “errors per active install, by version” a query rather than an investigation — and it is the one number that tells you whether 2.4.0 is worse than 2.3.0. The privacy constraints on what else may go in the payload are covered in reporting errors without breaking your privacy policy.

Two thresholds are worth wiring as alerts. The first is an absolute error rate on the new version above a small multiple of the previous version’s steady state. The second is the absence of expected events — a version that reports no successful syncs in its first hour is broken even if it reports no errors, because a worker that never starts cannot report anything.

The combination of a staged rollout and version-bucketed reporting is what makes the numbers in the incident timeline above achievable. Without either, the first signal is a review, and the rollout is already complete.

Errors per thousand active installs, by versionError rate for the three most recent versions during the first hours of a staged rollout, showing the new version standing out against its predecessors.2.2.0 (steady state)3 errors / 1k…2.3.0 (steady state)4 errors / 1k…2.4.0 at 10% rollout41 errors / 1…halt here2.4.1 hotfix4 errors / 1k…
With the version attached to each report, a bad release is visible at 10% rollout rather than at 100%.

Cross-browser variation

  • Chrome / Edge: the dashboard supports percentage rollouts for updates to published items, and halting one is immediate. Reverting to a previously published version is not a self-service action — it requires developer support and is not guaranteed.
  • Firefox (AMO): no staged rollout for most listings. You can re-upload a previous version under a higher version number, which is a revert in effect; review applies. Firefox’s review times are the most variable of the three, so a kill switch is worth proportionally more here.
  • Safari / App Store: phased release can be paused and the previous build remains available to users who have not updated, but there is no way to move an updated user back. Expedited review exists and is worth requesting for a genuine outage.
  • All three: unpublishing stops new installs and does nothing for existing ones. It is a way to limit growth of the problem, never a fix.

Verification

  1. Before you need it, confirm the levers exist: open each dashboard and find the rollout control, the version history and the expedited-review request. Knowing where they are saves more time than any runbook.
  2. Confirm the previous release is loadable:
1ls releases/ && npx web-ext lint --source-dir ./releases/v2.3.0

Execution context: your shell. An artifact that no longer lints is one you cannot fall back to — worth discovering now rather than mid-incident.

  1. Rehearse a downgrade: load the current build, use it, then load the previous build over it and confirm it starts without data errors.
  2. Confirm the kill switch works end to end by flipping the flag on your config endpoint and watching the extension pick it up within one alarm period.

FAQ

Can I force users off a broken version?

No. Extensions update on the browser’s schedule and there is no push mechanism. The closest thing is chrome.runtime.requestUpdateCheck, which asks the browser to look for an update now.

Should I unpublish while the fix is in review?

Only if the bug is harmful rather than merely broken. Unpublishing loses discovery and does not help existing users, and re-publishing can reset listing metrics.

How small should an emergency release be?

One commit, ideally. If the fix cannot be expressed as one commit on top of the released tag, the incident probably needs a considered release rather than an emergency one.

Other MV3 Architecture & Extension Lifecycle Resources