Surviving the MV3 service-worker 30-second timeout
Note · Surviving the MV3 service-worker 30-second timeout
- #chrome-extension
- #manifest-v3
Manifest V3’s background “service worker” gets killed after roughly 30 seconds of idle. There’s no reliable suspend event. Closures, in-memory caches, in-flight promises — gone.
Three patterns that survive this.
1 — chrome.alarms instead of setInterval:
// Won't survive — setInterval dies with the worker
setInterval(syncJiraDrafts, 60_000);
// Will survive — alarms wake the worker
chrome.alarms.create('sync-drafts', { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'sync-drafts') syncJiraDrafts();
});
2 — Persist on every state change, not on suspend:
async function setDraft(draft) {
await chrome.storage.session.set({ draft });
}
async function getDraft() {
const { draft } = await chrome.storage.session.get('draft');
return draft ?? { title: '', body: '' };
}
Don’t keep the draft as a module-scope variable. Treat the worker as stateless and
hydrate from chrome.storage.session on every wake. session (not local) clears on
browser restart, which is usually what you want for in-flight UI state.
3 — Long-lived port keepalive (use sparingly):
// Background
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'keepalive') return;
port.onDisconnect.addListener(() => { /* cleanup */ });
});
// Side panel / content script
const port = chrome.runtime.connect({ name: 'keepalive' });
This holds the worker open while the port is connected. Use it only when you genuinely need a continuous connection (a streaming response, a websocket bridge). Chrome has been progressively hostile to this pattern; assume it’ll break in some future Chrome release and budget for the cleanup.
The mental model that makes MV3 stop hurting: the worker is a stateless function
that mutates chrome.storage. Anything not in storage doesn’t exist between
invocations.