Checking for Updates with requestUpdateCheck

Control when an MV3 extension picks up new versions: how automatic updates work, runtime.requestUpdateCheck and its throttling, onUpdateAvailable, applying updates with runtime.reload, and when not to force them.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

You shipped a fix for a bug that is corrupting users’ data, and the store has approved it — but users keep reporting the bug for hours, because their browsers have not fetched the update yet. Or a support agent asks a user to “make sure you have the latest version”, and there is no button for that. Browsers update extensions on their own schedule, typically every few hours, and apply updates when it is safe to do so. MV3 gives the extension two levers: chrome.runtime.requestUpdateCheck to ask the browser to check now, and chrome.runtime.onUpdateAvailable with runtime.reload to apply a downloaded update promptly. This guide covers both and the limits that keep them from becoming a reload loop. It belongs to extension updates and data migration.

How automatic updates reach a user

Chrome checks the store’s update service for each installed extension periodically — every few hours — and on browser startup. When a newer version exists, it downloads the package in the background. Whether it then applies the update immediately depends on the extension’s state: if the extension is idle, Chrome installs the new version right away; if the extension is “in use” — its service worker is running with open ports, for example — Chrome may defer the install and fire runtime.onUpdateAvailable so the extension can choose when to restart. Applying an update restarts the extension: the service worker is replaced, extension pages close, and content scripts in open tabs are orphaned. requestUpdateCheck only shortens the first step, the check; it does not bypass store review or staged rollouts.

From store approval to the user's browserThe new version is approved, the browser's periodic check finds it hours later, downloads it, waits if the extension is busy, and installs it, restarting the extension.version approvednew version runningWait for checkup to hoursDownloadbackgroundDeferred if busyonUpdateAvailableInstall + …requestUpdateCheck can trigger the check hereonUpdateAvailable fires; reload when idle
requestUpdateCheck shortens only the wait for the check — not review or rollout.

Step-by-step: check, apply, and stay polite

1. Offer a manual “check for updates” in the UI

1// options.js
2checkBtn.addEventListener("click", async () => {
3  checkBtn.disabled = true;
4  const { status, version } = await chrome.runtime.requestUpdateCheck();
5  status === "update_available" ? showMessage(`Version ${version} is downloading — Readable will restart shortly.`)
6  : status === "no_update" ? showMessage(`You're on the latest version (${chrome.runtime.getManifest().version}).`)
7  : showMessage("Checked recently — try again in a few minutes.");          // "throttled"
8  checkBtn.disabled = false;
9});

Execution context: the options page (or popup). The call resolves with status — "update_available", "no_update" or "throttled" — and the available version. Chrome throttles frequent checks per extension; a throttled result is normal and should be shown as such, not as an error. Older Chrome versions use a callback form with the same values.

2. Listen for downloaded-but-deferred updates

1// sw.js — top level
2chrome.runtime.onUpdateAvailable.addListener(async ({ version }) => {
3  await chrome.storage.local.set({ pendingUpdate: { version, at: Date.now() } });
4  if (await safeToRestartNow()) chrome.runtime.reload();
5});

Execution context: the service worker. onUpdateAvailable fires when an update has been downloaded but not applied because the extension is busy. Calling chrome.runtime.reload() restarts the extension immediately with the new version. If you register no listener, the browser applies the update the next time the extension is idle, which is usually fine — handling the event is about applying it at a good moment rather than as soon as possible.

A deferred update applied at a safe momentThe browser downloads a new version while a long export job is running; onUpdateAvailable fires; the worker records it and waits; when the job finishes the worker calls runtime.reload and the new version starts.BrowserService worker v1Service worker v2onUpdateAvailable {version: 2.4.1}job running → deferjob done → runtime.reload()install + start v2onInstalled(rea…
Don't interrupt work in progress — finish it, then restart.

3. Define what “safe to restart” means

 1async function safeToRestartNow() {
 2  const all = await chrome.storage.local.get(null);
 3  const runningJobs = Object.entries(all).filter(([k, v]) => k.startsWith("job:") && v.state === "running");
 4  const openPages = await chrome.runtime.getContexts({ contextTypes: ["POPUP", "SIDE_PANEL", "TAB"] });
 5  return runningJobs.length === 0 && openPages.length === 0;
 6}
 7
 8chrome.storage.onChanged.addListener(async (c, area) => {
 9  if (area !== "local") return;
10  const { pendingUpdate } = await chrome.storage.local.get("pendingUpdate");
11  if (pendingUpdate && (await safeToRestartNow())) chrome.runtime.reload();
12});

Execution context: the service worker. Restarting closes every extension page the user has open and interrupts jobs. Waiting until no job is running and no extension UI is open avoids losing a half-written note or an export. runtime.getContexts (Chrome 116+) lists open extension contexts. Re-checking whenever storage changes catches the moment a job finishes.

4. Check after shipping an urgent fix

1// sw.js — on startup, if the server says this version is known-bad, check right away
2chrome.runtime.onStartup.addListener(async () => {
3  const { minSafeVersion } = await (await fetch("https://api.readable.example/v1/client-config")).json();
4  if (compareVersions(chrome.runtime.getManifest().version, minSafeVersion) < 0) {
5    await chrome.runtime.requestUpdateCheck();
6  }
7});

Execution context: the service worker. A small remote configuration value — data, not code — can tell old versions that an important fix exists, prompting an immediate check instead of waiting hours. Combine with disabling the broken feature through a remote flag until the update arrives, as described in remote feature flags without remote code.

Should the extension restart now?Decision tree for applying a downloaded update: if a job is running or extension UI is open, wait; if the update is a critical security fix, prompt the user; otherwise restart quietly when idle.Update downloaded — what's happening?job or UI activeWaitrecheck on changeRestart when idleruntime.reloadcritical fixAsk the user"Restart now" buttonReload on clickuser choseidleRestart quietlyruntime.reloadMigrations runonInstalled
Restart quietly when nothing is happening; ask when it would interrupt.

5. Avoid reload loops

1const { lastReloadAt = 0 } = await chrome.storage.local.get("lastReloadAt");
2if (Date.now() - lastReloadAt < 10 * 60_000) return;          // at most one forced reload per 10 min
3await chrome.storage.local.set({ lastReloadAt: Date.now() });
4chrome.runtime.reload();

Execution context: the service worker, guarding every programmatic reload. A bug that calls reload() on startup — for example, a version comparison that always thinks an update is pending — produces an extension that restarts forever, burning CPU and spamming the update server. A timestamp guard caps the damage.

6. Show what changed after the restart

When the new version starts, runtime.onInstalled fires with reason: "update" and previousVersion. That is where migrations run and where you can surface release notes, as described in showing a what’s new page after an update.

7. Know what you cannot speed up

requestUpdateCheck cannot bypass store review, staged rollout percentages, or enterprise policies that pin versions. Users in a rollout group that has not yet received the version get no_update. Self-hosted enterprise builds follow their own update_url and check interval.

Common mistakes

  • Treating throttled as an error. It is expected; explain it.
  • Reloading immediately on onUpdateAvailable. It closes open UI and interrupts jobs.
  • Unguarded runtime.reload(). One bug becomes an infinite restart loop.
  • Calling requestUpdateCheck on every startup. It is throttled and wasteful; check when there is a reason.
  • Expecting it to override staged rollouts. It will not.

Cross-browser variation

  • Chrome / Edge: requestUpdateCheck with throttling; onUpdateAvailable when the update is deferred; runtime.reload applies it.
  • Firefox: browser.runtime.requestUpdateCheck and onUpdateAvailable are supported for AMO-hosted and self-hosted updates; if onUpdateAvailable has a listener, Firefox waits for the extension to reload itself.
  • Safari: updates arrive with the containing app through the App Store; requestUpdateCheck is not meaningful there.

Verification

  1. Publish a test version (or use a self-hosted update_url in development) and click “Check for updates”: update_available.
  2. Click it again immediately: throttled.
  3. Start a long job, trigger an update, and confirm the restart waits until the job finishes.
  4. Confirm onInstalled runs with reason: "update" after the restart.

FAQ

How often does Chrome check for updates on its own?

Every few hours and at startup. The exact interval is not guaranteed.

Does an update require user approval?

Only if it adds permissions that produce new warnings; Chrome then disables the extension until the user accepts.

Can users see the update in chrome://extensions?

Yes — Developer mode shows an “Update” button that triggers checks for all extensions.

Can I tell users an update is waiting without restarting?

Yes — store the pending version from onUpdateAvailable and show a quiet “Update ready — restart Readable” line in the popup with a button that calls runtime.reload(). The user chooses the moment, which suits extensions that keep long-running UI open.

What happens to the old service worker during the update?

It is terminated and replaced. Any in-memory state is lost, which is one more reason every important value belongs in storage.

Other MV3 Architecture & Extension Lifecycle Resources