Development11 min read

The Complete Guide to Manifest V3 in 2026

Everything you need to know about Manifest V3 in 2026: service workers, declarativeNetRequest, storage changes, and migration strategies for Chrome extension developers.

C
CWS Kit Team
Share

Manifest V3 is no longer the future of Chrome extension development — it is the present, and the only path forward. As of late 2024, Google stopped accepting new Manifest V2 submissions entirely, and by January 2025, existing MV2 extensions began losing access to the Chrome Web Store. If you are building or maintaining a Chrome extension in 2026, MV3 is the only game in town.

But here is the thing most guides get wrong: MV3 is not just "MV2 with extra steps." The architecture has fundamentally shifted. Background pages are gone. Persistent state management works differently. Network request interception has an entirely new API surface. And the permission model is tighter than ever.

This guide covers everything you need to know to build confidently on Manifest V3 in 2026 — whether you are starting a new extension from scratch or maintaining one that was migrated in the past year.

Complete

MV2 Sunset

No new MV2 submissions since Nov 2024

250K+

Active MV3 Extensions

Majority of Chrome Web Store catalog

30 sec

Service Worker Idle Timeout

Down from unlimited in MV2 background pages

300K

DNR Rule Limit

Static + dynamic combined per extension

The Architecture Shift: Background Pages to Service Workers#

The single biggest change in MV3 is the replacement of persistent background pages with service workers. In MV2, your background script ran as long as the browser was open. You could hold state in memory, keep WebSocket connections alive, and rely on global variables persisting between events.

In MV3, your service worker spins up when needed and shuts down after roughly 30 seconds of inactivity. This changes everything about how you architect state.

What This Means in Practice#

Your service worker will be terminated and restarted constantly. Any in-memory state — variables, caches, connection objects — will be lost. You must treat your service worker as stateless and persist anything important to chrome.storage.

// BAD: In-memory state that will be lost when the service worker terminates
let userPreferences = { theme: "dark", notifications: true };
let cachedData: Record<string, unknown> = {};
 
chrome.runtime.onMessage.addListener((message) => {
  if (message.type === "GET_PREFS") {
    // This will return undefined after service worker restarts
    return userPreferences;
  }
});
 
// GOOD: Persist state to chrome.storage and hydrate on demand
async function getPreferences(): Promise<UserPreferences> {
  const result = await chrome.storage.local.get("userPreferences");
  return result.userPreferences ?? { theme: "dark", notifications: true };
}
 
async function setPreferences(prefs: Partial<UserPreferences>): Promise<void> {
  const current = await getPreferences();
  const updated = { ...current, ...prefs };
  await chrome.storage.local.set({ userPreferences: updated });
}
 
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message.type === "GET_PREFS") {
    getPreferences().then(sendResponse);
    return true; // Keep the message channel open for async response
  }
});

The return true pattern in message listeners is critical. Without it, the message channel closes before your async operation completes, and the sender gets undefined. This is the single most common bug in MV3 extensions, and it trips up even experienced developers. For a deeper dive into these pitfalls, see our post on service worker gotchas in MV3.

Alarms Replace setInterval#

Since setInterval and setTimeout cannot survive a service worker termination, use the chrome.alarms API for any recurring or delayed work:

// Register an alarm that fires every 15 minutes
chrome.alarms.create("sync-data", { periodInMinutes: 15 });
 
chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === "sync-data") {
    const data = await fetchLatestData();
    await chrome.storage.local.set({ syncedData: data, lastSync: Date.now() });
  }
});

The minimum alarm interval is 1 minute in production (30 seconds for unpacked extensions during development). If you need sub-minute polling, you will need to rethink your architecture — consider push-based approaches with chrome.gcm or firebase.messaging instead.

declarativeNetRequest: The New Network Interception Model#

The chrome.webRequest API in MV2 gave extensions the ability to observe and modify every network request passing through the browser. This was powerful but also a major performance and privacy concern — a single extension could silently inspect all traffic.

MV3 replaces most of this with chrome.declarativeNetRequest (DNR), a declarative rule-based system where you define rules and the browser executes them. You no longer get to run arbitrary JavaScript on each request.

Rule Types and Limits#

DNR supports several rule actions: block, redirect, upgradeScheme, modifyHeaders, and allowAllRequests. Rules are organized into rulesets:

  • Static rulesets: Defined in JSON files and declared in your manifest. Up to 100 rulesets total, with up to 50 enabled simultaneously. Combined limit of 300,000 rules.
  • Dynamic rules: Added at runtime via updateDynamicRules(). Limited to 30,000 rules.
  • Session rules: Like dynamic rules but cleared when the browser restarts. Also limited to 30,000.

For most ad blockers, content filters, and privacy extensions, this is sufficient. The uBlock Origin team famously struggled with the migration, but even they shipped a functional MV3 version (uBlock Origin Lite) that works within these constraints.

When You Still Need webRequest#

The webRequest API is not completely gone — you can still use webRequest.onBeforeRequest and other events in an observational capacity (without blocking). If you need blocking webRequest, your extension must declare the webRequestBlocking permission, which is now only available to policy-installed enterprise extensions. Consumer extensions on the Chrome Web Store cannot use it.

Do
  • Use declarativeNetRequest for blocking, redirecting, and header modification
  • Use webRequest (non-blocking) for logging and analytics only
  • Break large rule sets into multiple static rulesets and enable/disable them as needed
  • Use session rules for temporary filters that should reset on browser restart
Avoid
  • Request webRequestBlocking — it will be rejected for consumer extensions
  • Try to replicate MV2 webRequest logic 1:1 — redesign around declarative rules
  • Put all rules in dynamic rulesets when static would work — static rules are more performant
  • Exceed 30K dynamic rules — split logic between static and dynamic instead

Storage API Changes and Best Practices#

The storage APIs themselves have not changed dramatically in MV3, but how you use them has shifted because of the service worker architecture. Here are the key things to know in 2026:

chrome.storage.session#

Introduced alongside MV3, chrome.storage.session is an in-memory storage area scoped to the browser session. It is faster than chrome.storage.local for frequent reads because it avoids disk I/O, but its contents are lost when the browser closes. The default quota is 10 MB, which you can increase to unlimited by adding "storage" to your permissions.

Storage Quotas#

  • chrome.storage.local: 10 MB by default, unlimited with the "unlimitedStorage" permission.
  • chrome.storage.sync: 100 KB total, with a per-item limit of 8 KB. Maximum 512 items. Synced across the user's signed-in Chrome instances.
  • chrome.storage.session: 10 MB by default, unlimited with "storage" permission.

Batching Writes#

With service workers waking and sleeping constantly, you want to minimize storage operations. Batch your writes:

// Instead of multiple individual writes...
await chrome.storage.local.set({ key1: value1 });
await chrome.storage.local.set({ key2: value2 });
await chrome.storage.local.set({ key3: value3 });
 
// Batch them into a single call
await chrome.storage.local.set({
  key1: value1,
  key2: value2,
  key3: value3,
});

This is not just a style preference — each set() call triggers serialization, disk I/O, and fires onChanged listeners. Batching reduces overhead significantly, especially when your service worker might be terminated mid-operation.

Permissions in MV3: Tighter and More Transparent#

MV3 introduced host_permissions as a separate manifest key, pulled out of the permissions array. This separation matters because Chrome now lets users control host permissions independently — they can grant your extension access to specific sites instead of "all URLs."

The practical impact: you cannot assume your extension has host access just because the user installed it. Always check permissions at runtime with chrome.permissions.contains() before making cross-origin requests or injecting content scripts.

For a detailed breakdown of every permission, see our Chrome extension permissions explained guide.

Content Scripts and Scripting API#

In MV2, content scripts were typically declared statically in the manifest or injected with chrome.tabs.executeScript(). MV3 replaces tabs.executeScript() with the more powerful chrome.scripting API.

The scripting API offers three key methods:

  • scripting.executeScript() — inject JavaScript into a tab
  • scripting.insertCSS() / scripting.removeCSS() — inject or remove stylesheets
  • scripting.registerContentScripts() — dynamically register content scripts that persist across service worker restarts

That last one is important. Dynamic content script registration means you can conditionally inject scripts based on user settings without declaring every possible match pattern in your manifest. The registrations survive service worker termination, so you only need to set them up once.

Testing and Debugging MV3 Extensions#

Debugging MV3 extensions is slightly different from MV2. The service worker has its own DevTools inspector, accessible from chrome://extensions by clicking "Inspect views: service worker." Keep in mind that the DevTools connection itself keeps the service worker alive — so you will not naturally observe termination behavior while debugging.

Checklist

  • Test with DevTools closed to verify service worker restart behavior
  • Use chrome.storage.session for state that must survive restarts
  • Verify all message listeners return true for async responses
  • Check that alarms fire correctly after browser restart
  • Test declarativeNetRequest rules with the DNR debugging tools in DevTools
  • Confirm host permissions work in both 'on click' and 'on all sites' modes
  • Profile storage read/write performance under realistic data volumes
  • Test the extension after a Chrome update — API behavior can shift between versions

Migration Strategy for Existing Extensions#

If you still have an MV2 extension that somehow has not been migrated, the clock has fully run out. But even extensions that were hastily migrated in 2024 often have lingering issues — half-converted patterns, performance problems from naive storage usage, or broken functionality from the webRequest transition.

Here is a practical migration checklist:

Step 1: Audit Your Background Page#

Go through your background script line by line. Flag every piece of persistent state, every setInterval, every global variable, and every WebSocket connection. Each one needs a migration plan:

  • Global variables → chrome.storage.session or chrome.storage.local
  • setInterval / setTimeoutchrome.alarms
  • WebSocket connections → reconnect-on-wake pattern or move to a companion web app
  • XMLHttpRequestfetch() (this one is straightforward)

Step 2: Replace webRequest with declarativeNetRequest#

Map your existing webRequest listeners to DNR rules. This is the hardest part for most extensions because it requires switching from imperative logic to declarative rules. Some complex request modifications may not be possible — document what you lose and decide if it is acceptable.

Step 3: Update Permissions#

Move host permissions from permissions to host_permissions. Replace broad patterns with activeTab where possible. Add "scripting" to permissions if you use chrome.scripting.

Step 4: Test Aggressively#

The most common post-migration bugs are all related to the service worker lifecycle: messages that fail silently because the service worker is not running, state that disappears unexpectedly, and alarms that do not fire after a browser restart. Test every user flow with the service worker in a cold-start state.

For a broader look at what changed during the MV2 to MV3 transition, check out our migration breakdown.

Interactive tool

Chrome Extension Listing Audit

Audit your extension's store listing for SEO, compliance, and conversion — make sure your MV3 migration did not break your listing metadata.

Open tool

What is Coming Next#

Google continues to refine the MV3 APIs. In recent Chrome releases (130+), we have seen expanded DNR capabilities, better service worker lifecycle controls, and improvements to the offscreen API that lets extensions create off-screen documents for tasks that require DOM access (like audio playback or clipboard operations).

The offscreen API deserves special mention. It was introduced specifically to address gaps left by the removal of background pages. If your extension needs to parse HTML, play audio, or interact with the DOM in any way outside of a content script, chrome.offscreen.createDocument() is your escape hatch. Just be aware that only one offscreen document can exist at a time per extension, and it has its own lifecycle rules.

Looking further ahead, Google has signaled interest in:

  • Expanded userScripts API for extensions that manage user-defined scripts
  • Better support for long-running service worker tasks via the chrome.runtime.onSuspend event
  • Additional DNR action types for more granular request handling

Final Thoughts#

Building on Manifest V3 in 2026 is not harder than MV2 was — it is different. The constraints around service workers and declarative APIs push you toward cleaner architecture: explicit state management, event-driven design, and minimal permission footprints. Extensions that embrace these patterns end up more reliable, more performant, and more trustworthy to users.

The developer experience has also improved significantly since the rocky early days of MV3. Tooling has caught up, documentation is solid, and the ecosystem has largely settled. If you are starting a new extension today, you are in a better position than developers who migrated under pressure in 2024.

Build well, ship confidently, and if you need help getting your extension ready for the Chrome Web Store, our launch checklist and publishing guide have you covered.

Interactive tool

Permission Preview Tool

Preview how your extension's permissions will appear to users before you submit to the Chrome Web Store.

Open tool

Continue reading

Related articles

View all posts