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.

Key Takeaways
  • 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.onMessagechrome.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.

    Keep-Alive Pattern Reliability in MV3 Horizontal bar chart comparing five keep-alive strategies. Interval Ping: 0% reliability. WebSocket traffic: ~10% (very low). Native Host connectNative: ~15% (low). Port Keep-Alive runtime.connect: ~20% (low, requires reconnect loop). Offscreen Document hidden HTML messaging: ~50% (medium, but high memory cost). Source: Chromium bug trackers, StackOverflow, 2021-2025. Keep-Alive Pattern Reliability How likely each workaround actually keeps your SW alive 0% 20% 40% 60% 80% 100% Offscreen Doc Medium Port Keep-Alive Low Native Host Low WebSocket Very Low setInterval 0% Source: Chromium bug trackers, StackOverflow, GitHub Issues (2021–2025)
    Keep-alive pattern reliability based on documented failure reports across Chromium issue trackers and developer forums.

    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:

    1. T+0: Service worker handles an event
    2. T+5s: User closes laptop lid (system suspend)
    3. T+10min: User opens lid (system resume)
    4. T+10min+1ms: Chrome compares current time vs last event time
    5. Result: Delta is greater than 30 seconds
    6. 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:

    1. 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).
    2. 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).
    3. 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:

    JavaScript
    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.

    The Sawtooth Availability Pattern Line chart illustrating how MV3 service workers create a repeating connect-disconnect cycle. The worker connects, runs for 20-30 seconds, gets terminated, stays dead for 1-5 minutes, then reconnects on user interaction. This cycle repeats indefinitely, creating a sawtooth wave pattern. Source: Socket.io Issue 5117, GitHub, 2023. The Sawtooth Availability Pattern WebSocket connection state over time in MV3 Connected Dead 0s 30s 2min 2:30 5min 5:30 8min ~30s alive ~2min dead ~30s alive ~1min dead Source: Socket.io Issue #5117, GitHub (2023)
    The sawtooth availability pattern: MV3 service workers cycle between brief connected windows and longer dead periods, defeating real-time functionality.

    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:

    1. The native host (your desktop helper app) keeps running
    2. The service worker terminates
    3. The native host tries to write to stdout (the pipe to Chrome)
    4. Nobody’s listening on the other end. The data either generates a SIGPIPE or disappears
    5. When the service worker restarts, it spawns a new native host instance
    6. 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:

    1. User clicks “Login” in your extension popup
    2. Extension generates a nonce and stores it in a global variable
    3. User switches tabs to check their email
    4. Service worker idles out. Variable gone
    5. User clicks the verification link. Worker restarts
    6. 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).

    Network Protocol Compatibility: MV2 vs MV3 Grouped bar chart comparing five network protocols across MV2 and MV3. HTTP short requests: stable in both. HTTP long-poll: stable in MV2, broken in MV3 due to 30s termination. WebSocket: stable in MV2, unstable in MV3 with 1006 disconnects. WebRTC: stable in MV2, impossible in MV3 without offscreen doc. Native Messaging: persistent in MV2, ephemeral in MV3 with 5-minute limit. Source: Chromium documentation and bug trackers, 2021-2025. Network Protocol Support: MV2 vs MV3 Reliability rating from stable (100) to broken (0) 100 75 50 25 0 HTTP Short Long-Poll WebSocket WebRTC Native Msg MV2 (Persistent) MV3 (Ephemeral) Source: Chromium documentation and bug trackers (2021–2025)
    Network protocol reliability dropped sharply in the MV3 transition. Only short HTTP requests maintained full stability.

    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) or chrome.storage.local (persistent). Treat every wake-up as a cold start
    • Timers: use chrome.alarms exclusively. Never rely on setTimeout or setInterval
    • 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 onclose handler 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.storage backup
    •  Every onMessage handler 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.alarms replaces all setInterval usage
    •  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