Building a Chrome Extension with Manifest V3

TIL that MV3's move from persistent background pages to event-driven service workers means you can't hold long-lived in-memory state the way old extension tutorials assume — it gets killed and restarted constantly.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

Following an older Manifest V2 tutorial's pattern, I kept application state (a queue of extracted orders waiting to sync) as an in-memory variable in the background script. It worked for a few minutes at a time, then silently reset — the queue would just be empty again with no error.

Context #

The extension needed to buffer data extracted from the POS page and periodically sync it to the delivery API, which felt like a natural fit for a long-running background script holding a queue in memory.

What I Tried #

Assumed a bug in my sync logic was clearing the queue and added logging around every place I mutated it.

What Went Wrong #

The logs showed the queue was populated correctly and then the entire background script re-initialized from scratch, wiping the in-memory variable — because in MV3, the background script is a service worker, not a persistent page, and Chrome terminates and restarts it whenever it's been idle for a short window.

The Solution #

Moved the queue out of memory and into chrome.storage.local, which survives service worker restarts, and stopped assuming any background-script state would persist between events.

// MV3 background: don't trust in-memory state to survive
async function enqueueOrder(order) {
  const { queue = [] } = await chrome.storage.local.get("queue");
  queue.push(order);
  await chrome.storage.local.set({ queue });
}

Why It Works #

chrome.storage.local is durable across service worker lifecycle events by design, which is exactly the guarantee a plain JS variable in a service worker doesn't have. MV3's whole model assumes the background script is ephemeral and event-driven, not a persistent process you can lean on for in-memory state.

Lessons Learned #

Porting MV2 patterns to MV3 by just changing the manifest version number doesn't work — the background execution model is fundamentally different, and any code that assumed a persistent background page needs to be redesigned around a service worker's actual lifecycle.

What I Would Do Differently #

I'd read MV3's service worker lifecycle documentation before writing any background-script state logic, instead of porting an MV2 pattern and debugging the lifecycle difference the hard way.

MV3 service worker lifecycle, chrome.storage vs. in-memory state, event-driven background scripts.