Your extension works perfectly in development. You ship it. Then the bug reports start rolling in: “extension stopped working,” “lost my login session,” “WebSocket keeps disconnecting.” You check the logs. Nothing. The service worker just… died.
This isn’t a bug in your code. It’s the architecture working exactly as Chrome intended.
With the release of Chrome 150 in June 2026, Google removed the last remaining flags that allowed Manifest V2 extensions to run (9to5Google, 2026). The transition is final. Approximately 83.34% of the 272,697 extensions on the Chrome Web Store now run on MV3 (Chrome-Stats, 2026). Every one of them shares the same constraint: an ephemeral service worker that Chrome can kill whenever it wants.
This post breaks down the four ways MV3 service workers fail in production, why the popular workarounds don’t hold up, and what actually works in 2026.
- Chrome terminates MV3 service workers after 30 seconds of inactivity and enforces a 5-minute hard limit on any single event (Chromium Issue 40733525, 2022).
- WebSocket heartbeats didn’t reset the idle timer until Chrome 116; even now, system hibernation can trigger instant kills (developer.chrome.com, 2023).
- The popular “offscreen document ping-pong” hack reintroduces the memory bloat MV3 was supposed to eliminate (GitHub developer.chrome.com, 2025).
- Native messaging hosts become orphaned processes after 5 minutes, contradicting Chrome’s own documentation (GitHub Issue 2688, 2022).
What Changed Between MV2 and MV3?
In Manifest V2, your background page was a hidden browser tab. It had a persistent V8 isolate, a real DOM via the window object, and a lifecycle tied to the browser process itself. You could store a variable in window.myVar and it would survive for the entire browser session. Simple. Predictable.
MV3 replaced this with a service worker borrowed from the web platform but running under much stricter constraints. The service worker is ephemeral by design: it wakes up when an event fires, runs your code, and Chrome shuts it down.
Here’s the part that catches people off guard. Chrome’s task scheduler enforces two rigid timers:
- 30-second inactivity timer: if no Chrome Extension API event fires within 30 seconds, the worker gets a hard kill
- 5-minute maximum lifetime: no single event handler can keep the worker alive longer than 300 seconds
When these timers expire, it’s not a graceful shutdown. The V8 heap is destroyed. Network sockets are severed. In-memory state is gone. Chrome issues the equivalent of a SIGTERM to the worker’s renderer process (StackOverflow, 2021).
Why does this matter so much? Because Chrome’s model assumes all extension activity can be reduced to discrete, millisecond-scale transactions. If you’ve ever built a VPN client, a real-time security monitor, a file sync tool, or anything with a WebSocket connection, you already know that assumption falls apart immediately.
Why Does Chrome Kill Your Service Worker After 30 Seconds?
According to reports from AdGuard VPN’s MV3 migration, “if the extension doesn’t make any API calls or events within 30 seconds to prevent the service worker from falling asleep, the browser will simply stop the script” (AdGuard VPN Blog, 2024). This 30-second timer is the single most common cause of extension failures in production.
The timer doesn’t care about what your code is doing internally. Your setInterval running every 10 seconds? Ignored. Your active fetch request downloading a large file? Doesn’t count. Only Extension API events like chrome.runtime.onMessage, chrome.alarms.onAlarm, or chrome.webNavigation.onCompleted reset the clock.
The zombie state problem
There’s an even nastier failure mode lurking here. When Chrome updates your extension or you manually reload it, the service worker should cleanly tear down and restart. But a documented race condition causes this to fail. Chromium Issue 40805401 describes the scenario:
“It turns out that when you inspect chrome://serviceworker-internals/ we end up with TWO extension service workers and none of them works… The old one is ACTIVATED… the new one is INSTALLED and in WAITING WORKER state.”
Chromium Issue 40805401, 2021 (confirmed active through 2025)
The old worker holds a lock on IPC channels. The new worker can’t activate. Your extension icon stays visible but clicks do nothing. The only fix? A full browser restart.
This breaks silent auto-updates, which is supposed to be a core security feature.
Does System Sleep Break Your Extension?
Yes. And the mechanism is surprisingly simple: Chrome can’t tell the difference between “idle for 30 seconds” and “the laptop was closed for 10 minutes.”
A StackOverflow report documented this failure in detail: “Chrome apparently adds the hibernation time to the internal idle timer of the service worker… the same will happen even without the persistency trick if you suspend the PC… wait for more than 30 seconds” (StackOverflow, 2024).
Here’s the exact sequence that kills your worker:
- T+0: Service worker handles an event
- T+5s: User closes laptop lid (system suspend)
- T+10min: User opens lid (system resume)
- T+10min+1ms: Chrome compares current time vs last event time
- Result: Delta is greater than 30 seconds
- Action: Immediate termination of the service worker
This is an arithmetic error in Chrome’s delta-time calculation. The setInterval “keep-alive” trick fails here because intervals can’t fire during hibernation. When the system wakes, Chrome’s kill check runs before the JavaScript event loop can process the next interval tick.
So what does this mean in practice? Any extension that depends on continuous state, whether that’s an active VPN tunnel, a real-time chat client, or a background sync job, will break whenever a user closes their laptop for more than 30 seconds. That covers virtually every laptop user.
Which Keep-Alive Patterns Actually Work?
Developers have invented several workarounds to fight Chrome’s termination logic. Most of them are fragile. Let’s look at what the community has tried and what survives in 2026.
The offscreen document “ping-pong” hack
The most widely used workaround involves the chrome.offscreen API, originally intended for DOM scraping and audio playback. Developers create a hidden offscreen document that sends high-frequency messages to the service worker, tricking Chrome into thinking there’s real activity (GitHub – developer.chrome.com migration guide, 2025).
The offscreen document runs a setInterval loop every 20-30 seconds and sends a runtime.sendMessage. Chrome counts this incoming message as an “external event,” resetting the 30-second timer.
Sounds clever. But it has three serious problems:
- Memory bloat: the offscreen document is a full HTML renderer process. It consumes more memory than the service worker it’s protecting, reintroducing the exact problem MV3 claimed to solve (TanStack Router Discussion #1447, 2025).
- The 295-second reconnect storm: port connections hit a hard 5-minute cap. Developers must disconnect and reconnect every 295 seconds, creating periodic CPU spikes and a window where messages get lost (StackOverflow, 2021).
- Google might kill it: Chrome developers have signaled that this pattern is considered “abuse.” Future updates may clamp down on messages that don’t trigger real API activity (StackOverflow – chrome.tabCapture in MV3, 2022).
The chrome.alarms approach (recommended for 2026)
Since Chrome 116, the chrome.alarms API is the officially recommended way to periodically wake your service worker. Unlike setInterval, alarms persist across worker termination and will reliably restart the worker when they fire.
Combined with chrome.storage.session for ephemeral state and chrome.storage.local for persistent data, this gives you a viable architecture. It’s not persistence. It’s resilience. Your extension accepts that the worker will die and rebuilds state on each wake-up.
The tradeoff is latency. Alarms have a minimum period of 30 seconds, so you can’t poll more frequently than that. For many use cases, that’s fine. For real-time applications, it’s not.
Can WebSockets Survive in MV3?
Before Chrome 116, the answer was a flat no. WebSocket control frames (ping/pong) and even data frames didn’t count as “activity” for Chrome’s idle timer. The scheduler only looked for Extension API events, not raw TCP socket activity (Socket.io Issue #5117, 2023).
The result was what developers call the “sawtooth availability pattern”: connected for a few seconds, disconnected for minutes, reconnecting only when the user interacted with the extension. That completely defeats the purpose of background real-time alerts.
Socket.io’s disconnect loop is one of the most widely reported MV3 issues. Developers describe the service worker as repeatedly connecting and disconnecting at fixed intervals because “the disconnection reason on the client is ping timeout” (Socket.io Issue #5117, 2023). This sawtooth pattern affects any extension relying on persistent WebSocket connections for real-time notifications, trading feeds, or chat.
Chrome 116 improved this. WebSocket message exchanges now reset the idle timer, so a keepalive ping every 20 seconds can prevent termination (developer.chrome.com, 2023). You need to set "minimum_chrome_version": "116" in your manifest and implement a heartbeat:
function keepAlive() {
const keepAliveIntervalId = setInterval(() => {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
webSocket.send('keepalive');
} else {
clearInterval(keepAliveIntervalId);
}
}, 20 * 1000); // 20s < 30s idle timer
}But this still won’t save you from the hibernation bug. Close the laptop for more than 30 seconds and the timer math kills the worker on resume, regardless of your WebSocket state.
What Happens to Native Host Connections?
If your extension uses native messaging (think password managers, hardware integrations, or desktop companion apps), MV3 introduces a synchronization nightmare.
Chrome’s documentation initially claimed that chrome.runtime.connectNative would maintain the service worker’s liveness. Developer testing proved this claim false. The browser doesn’t treat an open pipe to a native process as a keep-alive signal (GitHub Issue 2688, 2022).
Chrome’s own documentation claimed that connectNative would keep the service worker alive. Testing by extension developers disproved this: “The claim ‘Chrome 100: native messaging port keeps service worker alive’ is untrue… within 5-6 minutes it becomes inactive and is disposed” (Chromium Issue 40733525, 2022). This confirms both the 5-minute hard limit and the orphan process problem.
Here’s what actually happens after 5 minutes:
- The native host (your desktop helper app) keeps running
- The service worker terminates
- The native host tries to write to stdout (the pipe to Chrome)
- Nobody’s listening on the other end. The data either generates a SIGPIPE or disappears
- When the service worker restarts, it spawns a new native host instance
- Now you have orphaned processes eating system resources
If you’ve ever wondered why your password manager’s extension occasionally fails to fill credentials or why your hardware integration stops responding, this is likely the root cause.
How Do You Prevent State Loss Across Restarts?
In MV2, you could store an auth token in a global variable and it would last the entire browser session. In MV3, that variable vanishes every time the service worker restarts, which could be every 30 seconds.
The prescribed fix is chrome.storage.session, but it’s asynchronous. That creates a subtle race condition: if the worker is terminating at the exact moment a storage write is requested, the write may fail silently. When the worker restarts, your state is corrupted (StackOverflow, 2022).
The OAuth flow disaster
Consider this real scenario documented across multiple StackOverflow threads:
- User clicks “Login” in your extension popup
- Extension generates a nonce and stores it in a global variable
- User switches tabs to check their email
- Service worker idles out. Variable gone
- User clicks the verification link. Worker restarts
- Extension tries to validate the nonce. It’s
undefined. Auth flow fails
Every multi-step workflow that relies on transient state is vulnerable to this. File uploads, multi-page form wizards, OAuth flows, payment confirmations, anything where the user might switch tabs between steps.
The “context invalidated” error
If you’ve worked with MV3 content scripts, you’ve seen this: Extension context invalidated. It happens when a content script tries to message a service worker that’s been replaced by a new instance (StackOverflow, 2018, still active in 2025).
In MV2, the persistent background page provided a stable anchor. In MV3, the high frequency of worker restarts increases the odds of a “context ID mismatch.” Every content script needs aggressive error handling to detect this state and re-establish connections. Most migration guides don’t mention this.
Why Does Your Wasm Code Fail in MV3?
MV3’s Content Security Policy prohibits remotely hosted code and restricts WebAssembly execution. If your extension runs anything computationally heavy (video processing, ML inference with transformers.js, OCR with tesseract.js), you’ve probably hit the “Blue Argon” violation.
The Blue Argon rejection code flags any reference to remote scripts, even trusted libraries like Firebase (Google Groups – Chromium Extensions, 2022).
For Wasm specifically, wasm-unsafe-eval was eventually added as a valid CSP directive, but the implementation is brittle. Tesseract.js and similar libraries load worker scripts via Blob URLs, which MV3’s CSP often blocks (tesseract.js Issue #601, 2025). The workaround is ugly: bundle massive Wasm binaries directly into your .crx package, bloating extension size and hitting Web Store limits (Medium – Transformers.js in MV3, 2025).
The practical consequence: you can’t hotfix your extension’s logic anymore. Every code change requires a full Web Store submission and review. For security tools that need to update filter lists or anti-phishing rules in response to zero-day threats, this latency is a real operational risk.
The Production-Ready Architecture for 2026
After analyzing the failure modes above, here’s the architecture pattern that works best with Chrome’s constraints rather than fighting them.
Accept ephemerality. Design for resurrection
Stop trying to keep the service worker alive. Instead, design every component to handle sudden death gracefully:
- State: persist everything to
chrome.storage.session(ephemeral) orchrome.storage.local(persistent). Treat every wake-up as a cold start - Timers: use
chrome.alarmsexclusively. Never rely onsetTimeoutorsetInterval - Messages: implement retry with exponential backoff in content scripts. Assume the worker might be dead when you send
- WebSockets: use the Chrome 116+ keepalive pattern with a 20-second heartbeat. But always have an
onclosehandler that reconnects with backoff - File uploads: chunk large operations into sub-30-second segments. Persist progress to storage between chunks
The resilience checklist
Before shipping any MV3 extension, verify these:
- Every global variable has a
chrome.storagebackup - Every
onMessagehandler has a try/catch that logs failures - Every content script handles
Extension context invalidated - WebSocket connections auto-reconnect with exponential backoff
- OAuth flows persist state to storage before user leaves
-
chrome.alarmsreplaces allsetIntervalusage - No Blob URL worker instantiation (use offscreen doc instead)
What makes this frustrating isn’t that MV3 is difficult. It’s that the difficulty comes from fighting the platform rather than building on it. The workarounds that the community has invented, the ping-pong hacks, the 295-second reconnect loops, the offscreen document abuse, these aren’t signs of bad engineering. They’re rational survival strategies inside a hostile execution environment.
Until Chrome provides a legitimate persistence mechanism for extensions that genuinely need it, this tension won’t resolve.
Edge Case Q&A
minimum_chrome_version to "116" and implement a heartbeat ping every 20 seconds. Before Chrome 116, WebSocket traffic was completely ignored by the timer (developer.chrome.com, 2023). chrome.alarms as your primary approach and reserve offscreen documents for their intended purpose (StackOverflow, 2022). Related Citations & References
- ISChromium
- STChrome extension MV3: persistent service worker die after wake up from hibernation – Stack Overflow
- GIdeveloper.chrome.com/site/en/docs/extensions/migrating/to-service-workers/index.md at main · GoogleChrome/developer.chrome.com · GitHub
- STjavascript – Persistent Service Worker in Chrome Extension – Stack Overflow
- GICache Routes to allow for Tabs to keep alive while offscreen · TanStack router · Discussion #1447 · GitHub
- STchromium – chrome.tabCapture unavailable in MV3's new offscreen API – Stack Overflow
- STjavascript – "A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received", What does that mean? – Stack Overflow
- GISocket.io client keeps disconnecting and reconnecting at a fixed interval because of ping timeout · Issue #5117 · socketio/socket.io · GitHub
- STjavascript – Chrome Extension Manifest V3: Try to import socket-io.client but keeps receiving error – Stack Overflow
- GIThe claim "Chrome 100: native messaging port keeps service worker alive" is untrue when connect(), onConnectExternal() used · Issue #2688 · GoogleChrome/developer.chrome.com · GitHub
- ISChromium
- STrest – How to securely implement authentication in Single Page Applications (SPAs) with a decoupled API – Stack Overflow
- STHow to avoid "Extension context invalidated" errors when messaging AFTER an Extension update? – Stack Overflow
- GRChrome Rejection Received with Violation Code: Blue Argon
- ISChromium
- GIReopen the question: will tesseract.js support chrome extension build with MV3 · Issue #601 · naptha/tesseract.js · GitHub
- MERunning Transformers Js Inside A Chrome Extension Manifest V3 A Practical Patch D7ce4d6a0eac
- ADAdGuard VPN Browser Extension migrated to Manifest V3




