How to Create a Service Worker for Chrome Extensions
Learn how to create and implement a service worker in Chrome extensions using Manifest V3. Includes examples, best practices, and use cases.
Creating a service worker for a Chrome extension in Manifest V3 requires registering a JavaScript file (e.g., service-worker.js) in manifest.json under the background.service_worker key.
Unlike legacy Manifest V2 background pages, a Chrome extension service worker is ephemeral. Chrome loads it on demand and terminates it when idle. To build a reliable service worker chrome extension, you must register event listeners synchronously at the top level of your script and persist all critical data in chrome.storage instead of memory.
Your debugger is likely lying to you. Inspecting a service worker in Chrome DevTools keeps it artificially alive, masking the most common bug in Manifest V3: state loss after suspension. Design for restart, not continuity.
Chrome Extension Service Worker Example: The Minimal Setup
Use type: "module" for new extensions to support standard ES imports. Keep importScripts() only for legacy code.
1. Create the manifest.json
Extensions register service workers in the manifest, not via navigator.serviceWorker.register().
{
"manifest_version": 3,
"name": "Minimal Worker",
"version": "1.0",
"background": {
"service_worker": "service-worker.js",
"type": "module"
}
}2. Create service-worker.js
Keep initialization lean.
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
chrome.storage.local.set({ isInitialized: true });
}
});Common Registration Failures:
- Wrong file path: Fails silently or throws a loading error (Status code 3).
- Legacy MV2 syntax: Using
background.scriptsinstead ofservice_worker. - Startup runtime errors: If top-level code throws an error, Chrome aborts registration (Status code 15).
What is a Chrome Extension Service Worker?
A Chrome extension service worker acts as the central background event dispatcher. It listens for browser events, runs isolated background logic, and shuts down to preserve memory.
Extension Service Worker vs. Web Service Worker vs. MV2 background.js
- Registration: Declared in
manifest.json, not invoked via the web navigator. - Lifecycle: Ephemeral. Wakes on extension events, not just fetch or push events.
- Capabilities: Full access to background extension APIs, but no DOM access and no
window.localStorage.
Treat this runtime like serverless compute. If you need DOM access, you must bridge to an offscreen document.
Lifecycle Rules: When Does Chrome Terminate the Worker?
Chrome proactively terminates your extension service worker under three conditions:
- Inactivity: ~30 seconds of idle time.
- Long-running tasks: A single event handler or API call blocks for > 5 minutes.
- Slow fetches: A network request takes > 30 seconds to resolve.
Crucial Version Updates:
- Chrome 110: Extension API calls now reset the 30-second idle timer, and the hard five-minute maximum lifetime was removed.
- Chrome 116: Active WebSocket connections extend the lifetime — sending or receiving messages across a WebSocket resets the service worker's idle timer.
- Chrome 120: The
chrome.alarmsAPI supports a 30-second minimum period.
Termination destroys global variables, timer handles (setTimeout, setInterval), and in-memory caches.
Storage: Surviving Suspensions and Restarts
Because the service worker js restarts constantly, never rely on global memory for critical state. If a content script expects a global let isAuthenticated = true inside the worker, that variable reverts to undefined after the next idle timeout.
chrome.storage.local: Durable data (caches, user profiles). Up to 10 MB (or unlimited with permissions).chrome.storage.session: Ephemeral runtime state (auth tokens, current session IDs). Up to 10 MB in-memory. Survives worker restarts but clears on browser close or when the extension is disabled, reloaded, or updated.chrome.storage.sync: Small cross-device user settings (Max 100 KB).
Restart-Safe Initialization Pattern:
Read from storage immediately on wake, then cache locally for synchronous access during that active session.
let runtimeCache = null;
async function getSafeCache() {
if (runtimeCache) return runtimeCache;
const data = await chrome.storage.session.get('cacheKey');
runtimeCache = data.cacheKey || {};
return runtimeCache;
}Event Listeners and Message Passing
Chrome wakes a suspended service worker the moment an event arrives. If you delay registering your listeners until an asynchronous setup finishes, the waking event will drop.
Register Listeners Synchronously at the Top Level:
// ✅ Correct: Registered immediately on wake.
chrome.action.onClicked.addListener(async () => {
const data = await chrome.storage.local.get('config');
// Handle click
});Sending Messages (onMessage)
To communicate between your service worker and content scripts, use chrome.runtime.sendMessage() or chrome.tabs.sendMessage().
If your chrome.runtime.onMessage handler performs asynchronous work before calling sendResponse, you must explicitly return true;. Otherwise, the message port closes immediately.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'FETCH_DATA') {
fetchDataAsync().then(sendResponse);
return true; // Keeps the messaging channel alive
}
});Replace setTimeout and setInterval with the chrome.alarms API, which wakes the service worker automatically.
Debugging: "Service Worker (Inactive)"
When you see "Service worker (inactive)" in chrome://extensions, it is not a bug. It is Chrome's expected idle behavior. Clicking the "service worker" link opens DevTools and immediately revives it.
The DevTools Trap: Keeping DevTools open pauses the idle timer. You will never experience termination bugs while inspecting the worker.
To test true real-world behavior:
- Load the extension and close DevTools.
- Wait 30 seconds for the worker to suspend.
- Trigger an extension action (like a popup or message) and verify your state recovers correctly.
For automated testing, use Puppeteer to programmatically disconnect the worker (await worker.close()) and verify the extension survives the restart.
How to Keep a Chrome Extension Service Worker Active
Chrome does not support broadly persistent service workers in MV3. Do not deploy vague keepalive ping hacks (like nested timeouts); they drain battery and invite Web Store rejection.
Design for suspension first. Only use official keepalive mechanisms for legitimate, specialized use cases:
- WebSockets: Chrome 116+ keeps the worker alive as long as WebSocket messages are being sent or received (ideal for real-time chat or AI agents).
- Native Messaging: Connecting to a desktop app via
chrome.runtime.connectNative()maintains the worker. - Audio/Screen Recording: Route this through an offscreen document, which manages its own lifecycle.
Migrating background.js to Manifest V3
Treat MV3 migration as an architecture rewrite, not a file rename.
- Update
manifest.json: Changebackground.scriptstobackground.service_worker. - Replace
XMLHttpRequest: Usefetch(). - Remove DOM Parsing: Move
DOMParseror canvas logic to an offscreen document. - Offload Timers: Replace
setIntervalwithchrome.alarms.create(). - Relocate State: Move global variable states to
chrome.storage.session.
If supporting cross-browser compatibility (like Firefox), declare both keys, as Firefox still utilizes scripts for MV3 fallbacks:
"background": {
"service_worker": "service-worker.js",
"scripts": ["service-worker.js"]
}Using a Service Worker for SDKs and Monetization
The service worker is the correct execution context to initialize background SDKs. For example, if you integrate an open-source monetization platform like Mellowtel to fund your extension via unused bandwidth, initialize it in the background script.
import Mellowtel from 'mellowtel';
// Initialize in global scope
const mellowtel = new Mellowtel("YOUR_EXTENSION_KEY");
await mellowtel.initBackground();Because variables reset, always cache users' opt-in or premium subscription statuses in chrome.storage.local. Fetching a subscription on load and saving it to a global variable means users revert to "free" tier status after 30 seconds of inactivity.
Keep consent UI in the popup or an onboarding tab—never hide it in the background.
FAQ
How does a Chrome extension service worker update?
The worker updates when you publish a new extension version. Manifest V3 forbids fetching remote hosted code for extension logic. Your service worker must be securely bundled within the package you ship.
Why can't I use localStorage in an extension service worker?
Service workers lack DOM access, including the Window object and localStorage. Use chrome.storage.local or chrome.storage.session instead.
What causes service worker registration to fail?
Status code 3 indicates a broken import or incorrect file path. Status code 15 means top-level code tried to access an undefined property during initialization.
Building a resilient service worker chrome extension comes down to respecting its ephemeral nature. Store your state externally, register event listeners at the top level, keep asynchronous message channels open with return true, and test your code with the DevTools pane closed.