Manifest V3 Migration Guide for Chrome Extension Developers
Chrome’s transition from Manifest V2 to Manifest V3 has been the most contentious extension platform change in the last decade. The migration window has stretched longer than Google originally projected, but by mid-2026 the practical reality is straightforward: if you maintain a Chrome extension, you are either on V3 already or you are running on borrowed time. This guide walks through what actually changed, what breaks, and how to plan the migration without rewriting your extension twice.
What Actually Changed Between MV2 and MV3
The headline changes are well-documented, but the way they interact in practice is where most migrations get stuck. Three shifts matter more than the rest.
Background pages became service workers. In MV2, a background page was a persistent HTML document with its own DOM, long-lived global state, and the ability to hold references indefinitely. In MV3, the background context is a service worker — event-driven, terminated when idle (typically after 30 seconds of inactivity), and without access to the DOM. Anything you stored in a module-level variable will be gone the next time the worker wakes up. This single change forces architectural decisions throughout the extension.
webRequest blocking was replaced by declarativeNetRequest. The old webRequest API let extensions intercept network requests, inspect them, and modify or block them in JavaScript. declarativeNetRequest (DNR) instead requires you to declare rules ahead of time and let the browser apply them. Observational use of webRequest is still permitted, but the blocking variant is gone for most extension types. This is the change content blockers and privacy extensions have fought hardest, and the practical limits of DNR — particularly its dynamic rule cap and lack of regex on some fields — are real constraints worth measuring before you commit to a design.
Host permissions are now runtime-grantable. Permissions listed in host_permissions can be granted at install time or deferred until the user explicitly approves them through chrome.permissions.request(). Combined with the new “Sites you allow on click” model, this changes how you should think about onboarding. Users who installed your extension expecting it to “just work” on every site may now see a permission prompt the first time it tries to do so.
Service Workers: The Practical Consequences
The service worker change rewrites how you structure persistent state. A few patterns that worked in MV2 will silently break in MV3:
- Module-level caches.
const cache = new Map()at the top of a background script in MV2 lived for the life of the browser session. In MV3, it lives until the next time the worker is terminated, which could be 30 seconds from now. setTimeoutandsetInterval. These do not survive worker termination. If you need delayed work, usechrome.alarmswith a minimum period of 30 seconds (or 1 minute on some channels).- Long-lived
portconnections. Aportfrom a content script can keep the worker alive, but only while the port is open. Disconnect handlers fire, but you cannot assume your worker is around to receive a follow-up message. - WebSockets and SSE. These were a common MV2 pattern for real-time extensions. In MV3, an open WebSocket counts as activity and will keep the worker alive — but if the socket closes, the worker will terminate, and reconnection logic needs to live somewhere durable (an alarm-triggered reconnect is the conventional pattern).
The mental model that works: treat the service worker as a pure function over chrome.storage. If a piece of state needs to survive worker termination, it lives in storage. If it doesn’t, it’s transient by definition.
declarativeNetRequest: What You Get and What You Lose
DNR is more capable than the early MV3 previews suggested, and most extensions that block, redirect, or modify a moderate number of requests can use it without much friction. The constraints that bite hardest are:
- Static rule limits. Each extension can ship a finite number of static rules, with a global guaranteed minimum and a higher “unsafe” tier. For content blockers shipping large filter lists, this means splitting rulesets and using
enableRulesetsto swap them at runtime. - Dynamic rule cap. Rules added at runtime via
updateDynamicRulesare capped at 30,000 in current Chrome (up from 5,000 in early MV3). For most extensions this is fine; for filter-list-driven blockers it is the binding constraint. - No JavaScript decisioning. You cannot inspect a request body and decide based on its contents. If your extension’s value proposition depends on programmatic per-request logic, DNR will not replace it — and your options are observational
webRequestplus user-side mitigation, or a different architecture entirely.
If you are migrating a content blocker or privacy tool, our browser fingerprinting explainer covers the broader context of what these APIs can and cannot do for user privacy.
A Migration Checklist That Works in Practice
The order matters. Doing these in sequence saves rework:
- Audit your
manifest.json. Identify every MV2-only field:background.scriptsandbackground.persistent,browser_action/page_action(now unified asaction),content_security_policyas a string (now an object), andweb_accessible_resourcesas a flat array (now an array of objects withmatches). - Inventory background state. Grep for module-level variables,
setTimeout,setInterval, and any global references. Each one is a migration decision. - Inventory
chrome.webRequestusage. Separate blocking from observational. Plan DNR rules for the blocking cases; keep observational use as-is. - Inventory host permissions. Decide which sites are core (request at install) and which are opportunistic (request at runtime).
- Move long-lived state to
chrome.storage.sessionorchrome.storage.local.sessionis in-memory and cleared on browser restart;localpersists. Pick deliberately. - Replace timers with
chrome.alarms. Anything that needs to fire later than the worker’s idle timeout must use alarms. - Test the cold-start path. The most common MV3 bug is code that works when the worker is warm and fails when it has just been spun up. Disable your extension, re-enable it, and exercise every entry point.
- Test under memory pressure. Chrome will aggressively terminate idle workers when memory is tight. Simulate this by manually stopping the worker from
chrome://extensionsand verifying the next event wakes it correctly.
Firefox MV3 Compatibility: Where It Diverges
Firefox shipped MV3 support but made different choices on the most contentious pieces. The practical differences for cross-browser extensions:
- Background scripts are still allowed. Firefox supports both persistent background pages and event pages in MV3, alongside service workers. If your extension targets Firefox primarily, you can keep a more MV2-like architecture.
webRequestblocking is still available. Firefox did not remove the blocking variant. Extensions that need programmatic request decisioning can continue to use it on Firefox even after losing it on Chrome.- DNR is supported but with quirks. Rule limits and supported conditions differ. Test your rulesets on both browsers; do not assume parity.
browser.*namespace and promises. Firefox’s WebExtensions API has always returned promises and used thebrowser.*namespace. Chrome’schrome.*API now also returns promises in MV3, narrowing the gap, but a polyfill is still the cleanest path for shared codebases.
For a deeper look at where the browsers actually agree and disagree, see our WebExtensions API cross-browser compatibility guide.
Timeline Considerations
Google’s MV2 sunset has slipped repeatedly, but the direction has never reversed. Enterprise policy extensions can hold MV2 longest; consumer extensions distributed through the Chrome Web Store have effectively been V3-only for new submissions for some time. If you are planning a migration in 2026, the honest assessment is:
- Do not invest in MV2-only improvements.
- Treat MV3 as the baseline for all new code.
- If you are still on MV2, the migration is no longer optional — the only question is whether you do it deliberately or under pressure when your listing is removed.
FAQ
Can I keep using webRequest for observational purposes in MV3?
Yes. The non-blocking variants (onBeforeRequest without "blocking", onCompleted, etc.) remain available. Only the blocking and modifying variants were removed for most extension categories.
How many DNR rules can I ship? Static rules have a guaranteed minimum across all enabled rulesets, with a higher “unsafe” tier that counts against a global pool. Dynamic rules are capped at 30,000 in current Chrome. Session rules add another 5,000. The exact static numbers shift between Chrome versions, so check the current documentation before designing around the limits.
Will my MV2 extension stop working overnight? No — the deprecation has been phased. Existing installs typically continue working until a forced disable date is announced for your channel. New submissions and updates require MV3.
Is there a polyfill that makes MV3 service workers feel like persistent background pages? There are libraries that offer storage-backed state and alarm-based timers as drop-in replacements, but none can restore true persistence. The architectural shift is the point of MV3, and working with it is more sustainable than working around it.
Should I write new extensions targeting MV3 only, or maintain MV2 compatibility? For new extensions in 2026, target MV3. The browsers that still accept MV2 are a shrinking audience, and the maintenance cost of dual-targeting outweighs the reach gain.