Duskel Start a project
Blog/Chrome Extensions

Migrating a Chrome extension from Manifest V2 to V3

Manifest V2 is dead and V3 breaks assumptions your extension was built on. Here is what actually breaks, why, and how to migrate it without shipping a regression.

Duskel·27 Jul 2026·13 min read·Chrome Extensions

If you have a Chrome extension still on Manifest V2, the clock already ran out. Chrome disables V2 extensions, the Web Store stopped accepting new V2 submissions long ago, and enterprise policy exemptions that kept some alive are gone too. This is not a "should we plan for it" conversation anymore; it is a "your extension either got migrated or it stopped working" one.

The frustrating part is that V3 is not a cleaner version of the same model. It changed the runtime assumptions your extension was written against. We have shipped V3 migrations that looked like a config change on paper and turned into a rewrite of how the extension holds state, so let us walk through what actually breaks and how to move it across without introducing bugs your users find before you do.

Is Manifest V2 dead in 2026?

Short answer: yes. For the general Chrome user base, Manifest V2 support is over — Chrome has moved through disabling V2 extensions and turning them off entirely, the Web Store hasn't accepted new V2 submissions since 2022, and the enterprise policy that let managed environments keep V2 running has now expired as well. If your extension is still V2, it isn't "about to break"; for most of your users it already stopped loading.

And no, there is no "Manifest V2 to V3 converter" that does this for you. People search for one, but a tool cannot exist for the hard part, because V3 didn't rename the manifest — it changed the runtime your code runs on. A converter could bump the version number and rename a couple of fields; it cannot move your in-memory state into storage or turn your blocking request logic into declarative rules, which is the actual work. Below is what that work really involves.

The one change everything else follows from

In V2 you had a background page. It was an invisible HTML page that stayed alive for as long as the browser was open. You could set a variable on it, open a WebSocket, start a timer, cache a value, and trust it would all still be there ten minutes later. Extensions leaned on that hard, usually without anyone deciding to — it was just how things worked.

V3 replaces the background page with a service worker, and the service worker is ephemeral. Chrome starts it when there is an event to handle and kills it shortly after, often within thirty seconds of going idle. Every migration headache below is a consequence of this single fact: the thing you used to treat as always-on is now something that wakes up, does one job, and dies. If you internalise nothing else, internalise that the background context is no longer allowed to remember anything on its own.

What actually breaks

Persistent state is the first casualty. Any global variable, in-memory cache, or object you set on the background page and expected to read later is gone the moment the worker sleeps. Counters reset, login state evaporates, and the "it works on my machine" demo passes because your worker never idled long enough to die. This is the bug that ships silently. Everything you kept in memory has to move to chrome.storage, and every read of it has to assume it might be starting from cold.

Timers are the second. setTimeout and setInterval do not survive the worker being killed, so anything you scheduled for later may simply never fire. Long-running intervals, polling loops, and "do this in five minutes" logic all break. The replacement is the alarms API, which asks Chrome to wake your worker at a time you specify — the scheduling lives outside your code because your code is not guaranteed to be running when the moment arrives.

Blocking webRequest is the third and the most disruptive if you relied on it. In V2 you could intercept a network request and modify or cancel it in your own JavaScript. V3 removes that blocking power for most extensions and replaces it with declarativeNetRequest, where you declare rules up front and Chrome enforces them without running your code per request. Ad blockers, header rewriters, and request filters all have to be rewritten as static or dynamic rule sets. If your extension's whole reason to exist was inspecting requests in code, this is not a port, it is a redesign, and you need to check your logic even fits the declarative model before promising a date.

Two smaller ones catch people out. Remotely hosted code is banned — you can no longer pull JavaScript from a server and eval it, so anything that loaded logic at runtime has to ship inside the package. And executeScript now injects functions or files rather than arbitrary code strings, so any string-based script injection has to be rewritten.

The migration checklist

Before you touch the manifest, it helps to see the whole job on one page. This is the checklist we run on every Chrome extension migration we take on — each row is a place V3 changed the rules, what you have to do about it, and roughly how much work it is. The high-effort rows are the ones that decide whether your migration is a week or a month.

V2 concernWhat changes in MV3Effort
Background pageBecomes an ephemeral service worker that Chrome kills when idleHigh
Persistent globals / in-memory cacheMove to chrome.storage; every read assumes a cold startHigh
Blocking webRequestRewrite as declarativeNetRequest rule sets; may not map cleanlyHigh
setTimeout / setIntervalReplace with the alarms API for anything beyond a few secondsMedium
Long-lived message portsTolerate the worker dropping mid-conversation and reconnectingMedium
Remotely hosted codeBan lifted — bundle every bit of logic inside the packageMedium
executeScript with code stringsInject functions or files instead of arbitrary stringsLow
browser_action / page_actionCollapse into a single action entryLow
Host permissions in permissionsMove to host_permissions and narrow the scopeLow
manifest_version: 2Bump to 3 — do this last, not firstLow

How to migrate, step by step

Start by auditing, not editing. Read your manifest and list every permission, every background responsibility, and every place you assumed the background page persisted. Grep for setTimeout, setInterval, global state on the background, webRequest listeners, and any runtime code loading. That list is your actual scope. The manifest version bump is the last thing you do, not the first.

Convert the manifest next: manifest_version to 3, background page to a service worker entry, browser_action and page_action collapse into action, and host permissions move into their own field. This part is mechanical and fast, and it is also the part that fools people into thinking the job is nearly done. It is not; it is the part that makes the real problems compile.

Now move state out of memory. Every value the background used to hold goes into chrome.storage, and every function that read those values becomes async and defensive about the store being empty on a cold start. Rewrite timers onto the alarms API. Rewrite webRequest logic as declarativeNetRequest rules, and if it does not map cleanly, that is a signal to redesign that feature rather than force it. Replace any remote code with bundled code and any string injection with function or file injection. Concretely, the two most common rewrites look like this:

jscopy
// V2: a global on the always-on background page.
// Gone the moment the idle service worker is killed.
let seenCount = 0;
chrome.runtime.onMessage.addListener(() => { seenCount++; });

// V3: state lives in chrome.storage; every read assumes a cold start.
chrome.runtime.onMessage.addListener(async () => {
  const { seenCount = 0 } = await chrome.storage.local.get('seenCount');
  await chrome.storage.local.set({ seenCount: seenCount + 1 });
});

// V2: a timer that silently stops firing once the worker is killed.
setInterval(syncNow, 5 * 60_000);

// V3: ask Chrome to wake the worker on a schedule instead.
chrome.alarms.create('sync', { periodInMinutes: 5 });
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'sync') syncNow();
});

Message passing needs a second look too. Because the worker can die mid-conversation, long-lived connections and multi-step message flows have to tolerate the other end disappearing and reconnecting. Assume any port can drop and any handler can be running in a freshly-woken worker with nothing in memory.

What a V2-to-V3 migration costs and how long it takes

The honest answer is that it depends entirely on how much your extension leaned on the persistent background page and blocking webRequest — the two things V3 took away. A migration is not a from-scratch build, so it is cheaper than the original, but it is also untangling code you may not have written, so it is not a find-and-replace either. Here is roughly where the tiers land; the full migration cost breakdown goes deeper on what sets the number.

TierWhat it looks likeTimelineFrom
SimpleNo real background state, no webRequest, a short permission list. A mostly mechanical manifest bump plus light state moves.2–4 daysfrom $1k
MediumGenuine background state to move into storage, timers onto alarms, message flows to harden, and host permissions to narrow.1–2 weeksfrom $3k
ComplexBlocking webRequest or an ad-blocker-style feature rebuilt as declarativeNetRequest, or logic that does not map to the declarative model and needs redesign.3–6 weekscase by case

What moves you up the table is almost never the manifest edit — it is the amount of state hiding in the old background page and whether any feature depended on inspecting requests in code. If your extension just restyles pages or exposes a popup utility, you are at the bottom of the table. If it filtered or modified network traffic, budget for the top. When you are not sure which tier you are in, tell us what your extension does and we will scope it against your actual code rather than a guess.

Testing and getting back through review

The trap in testing V3 is that a healthy worker hides your bugs. During active use the worker stays warm, your in-flight state survives, and everything looks fine. You have to force the failure: let the worker idle until Chrome kills it, or stop it by hand in the extension's service worker inspector, then interact and confirm nothing lost its place. If your extension still works after you have manually killed the worker between every action, the state migration is real. If it only works when you click quickly, it is not.

Then re-test the rewritten features specifically. Confirm alarms fire after the worker has slept, confirm your declarativeNetRequest rules actually match the requests the old code caught, and confirm cold-start reads from storage behave when the store is empty. These are the three areas where a migration ships a regression, so they get deliberate tests rather than a quick click-through.

Finally, expect the Web Store to look again. A V3 resubmission is a fresh review, and a changed permission set or a new host permission can trigger manual review that takes days. Keep the permission list as narrow as the new APIs allow — declarativeNetRequest often lets you drop the broad host access V2 needed, which can make your V3 version review faster than the original. Migrate early, submit with room before any deadline you care about, and do not assume approval is instant just because the extension already existed. Before you resubmit, run an automated policy check: our open-source storecheck lints an extension against Web Store policy and flags exactly these traps — leftover eval, over-broad host permissions, a permission list wider than the stated purpose — before a reviewer does. If you would rather have the migration audited and shipped for you, tell us what your extension does and we will scope it.

Common MV3 rejection traps. These are the ones that get a resubmission held or bounced: requesting broad host permissions the new APIs no longer need (declarativeNetRequest usually lets you drop all-sites access); any leftover remotely hosted code or eval, which V3 bans outright; a permission list wider than the listing's stated purpose justifies; and a privacy policy that does not cover the data your manifest still says it can touch. Trim every permission down to what the V3 APIs actually require before you resubmit.

FAQ

How long does a Manifest V2 to V3 migration take?

A simple extension — no persistent background state, no blocking webRequest, a short permission list — is usually 2 to 4 days. One with real background state to move into storage, timers to shift onto alarms, and message flows to harden is typically 1 to 2 weeks. If it relied on blocking webRequest or an ad-blocker-style feature that has to be rebuilt as declarativeNetRequest, budget 3 to 6 weeks, because that part is a redesign rather than a port.

How much does it cost to migrate a Chrome extension to Manifest V3?

A straightforward migration starts around $1k, a medium one with genuine background state and permissions to narrow from around $3k, and a complex one involving declarativeNetRequest or a feature that does not map to the declarative model is priced case by case. The cost is driven almost entirely by how much state hid in the old background page and whether any feature inspected network requests in code — not by the manifest edit itself.

Why do Manifest V3 resubmissions get rejected?

The frequent causes are broad host permissions the V3 APIs no longer need, leftover remotely hosted code or eval (banned in V3), a permission list wider than the listing's stated purpose, and a privacy policy that does not cover the data the manifest can touch. Trimming permissions to exactly what declarativeNetRequest and the other V3 APIs require usually makes the V3 version review faster than the original did.

Can you just bump manifest_version from 2 to 3?

No. The version bump is mechanical and takes minutes, but it only makes the real problems compile. V3 replaces the always-on background page with an ephemeral service worker, so any state you kept in memory has to move to chrome.storage, timers move to the alarms API, and blocking webRequest becomes declarativeNetRequest. Change the number without moving the state and the extension will work in your testing and fail silently for a share of your users.

Is Manifest V2 still supported in Chrome in 2026?

No. Manifest V2 support has ended for the general Chrome user base — Chrome has moved through disabling and then turning off V2 extensions, the Web Store stopped accepting new V2 submissions back in 2022, and the enterprise policy that let managed environments keep running V2 has now expired too. If your extension is still on V2, it isn't about to break in the future; for most users it has already stopped loading. The only path forward is migrating to V3.

Is there a Manifest V2 to V3 converter tool?

Not for the part that matters. A tool can bump manifest_version to 3 and rename a couple of fields, but it cannot do the real work, because V3 didn't rename the manifest — it changed the runtime your code runs on. No converter can move your in-memory background state into chrome.storage, replace your timers with the alarms API, or turn blocking webRequest logic into declarativeNetRequest rules, because those require understanding what your extension does. Treat any "one-click converter" as, at best, the first five minutes of the job.

Written by Duskel

A software studio that ships and maintains its own products — KeepChats, Gwora and MoveProof — and builds the same way for clients. Founded and led by codewithumar.

Talk to the studio →
RELATED READING
Chrome Extensions · 9 min read

How to hire a Chrome extension development company that ships

Chrome Extensions · 11 min read

Chrome Web Store rejection: why extensions get rejected and how to fix it

Chrome Extensions · 8 min read

How much does a Manifest V3 migration cost?

CHROME EXTENSIONS

Stuck on a V2 extension that needs to move to V3? We do exactly this.

Send the problem. You get one fixed number and a plan back within a business day.

Duskel
Duskel
AI AUTOMATIONSOFTWARE

We build software worth keeping — for clients, and for ourselves.

Founded & led by codewithumar

© 2026 Duskel. All rights reserved.DUSKEL SMC-Private Limited · Incorporated 2021 · Lahore, PakistanBuilt to last, not to demo.