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.
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.
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.
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.
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
throttledas 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
requestUpdateCheckon 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:
requestUpdateCheckwith throttling;onUpdateAvailablewhen the update is deferred;runtime.reloadapplies it. - Firefox:
browser.runtime.requestUpdateCheckandonUpdateAvailableare supported for AMO-hosted and self-hosted updates; ifonUpdateAvailablehas a listener, Firefox waits for the extension to reload itself. - Safari: updates arrive with the containing app through the App Store;
requestUpdateCheckis not meaningful there.
Verification
- Publish a test version (or use a self-hosted
update_urlin development) and click “Check for updates”:update_available. - Click it again immediately:
throttled. - Start a long job, trigger an update, and confirm the restart waits until the job finishes.
- Confirm
onInstalledruns withreason: "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.
Related
- Controlling when an update is applied — deferral in depth.
- Staged rollouts in the Chrome Web Store — why some users get updates later.
- Fixing extension context invalidated after an update — what restarts do to open tabs.
- Extension updates and data migration — the parent topic.