Solution 1:

Solutions:

  • If the event are likely to occur rarely e.g. several ties a day, rework your extension to save/load the variables/state in each listener via chrome.storage.session (temporary, 1MB max), or chrome.storage.local, or even IndexedDB (much faster for big/complex data).

  • Use ManifestV2 if possible because there is no such thing as a persistent service worker.

  • Use the below workarounds to prevent the extension's service worker from unloading.

These bugs in ManifestV3 may be related:

  • crbug.com/1024211, the worker doesn't wake up for webRequest events (see workarounds).
  • crbug.com/1271154, the worker is broken after an update (solved in another answer).

Workarounds:

0. Five minutes, via waitUntil

  • Example 1, a hardcoded timeout:

    self.onactivate = e => {
      e.waitUntil(new Promise(resolve => setTimeout(resolve, 5 * 60e3)));
    };
    
  • Example 2, waiting for a promise:

    let allDone;
    self.onactivate = e => e.waitUntil(new Promise(r => { allDone = r; })));
    //................
    chrome.some.event.addListener(() => {
      foo().bar().finally(allDone);
    });
    

1. "Forever", via runtime ports, while a connectable tab is present

Open a runtime port from any tab's content script or from another page of the extension like the popup page. This port will live for five minutes (the inherent limit of a service worker) so you'll have to use a timer and the port's onDisconnect event to reconnect with some random tab again.

Downsides:

  • The need for a web page tab or an extension tab/popup.
  • Broad host permissions (like <all_urls> or *://*/*) for content scripts which puts most extensions into the slow review queue in the web store.

Implementation example:

  • manifest.json, the relevant part:

      "permissions": ["scripting"],
      "host_permissions": ["<all_urls>"],
      "background": {"service_worker": "bg.js"}
    
    
  • background service worker bg.js:

    let lifeline;
    
    keepAlive();
    
    chrome.runtime.onConnect.addListener(port => {
      if (port.name === 'keepAlive') {
        lifeline = port;
        setTimeout(keepAliveForced, 295e3); // 5 minutes minus 5 seconds
        port.onDisconnect.addListener(keepAliveForced);
      }
    });
    
    function keepAliveForced() {
      lifeline?.disconnect();
      lifeline = null;
      keepAlive();
    }
    
    async function keepAlive() {
      if (lifeline) return;
      for (const tab of await chrome.tabs.query({ url: '*://*/*' })) {
        try {
          await chrome.scripting.executeScript({
            target: { tabId: tab.id },
            function: () => chrome.runtime.connect({ name: 'keepAlive' }),
            // `function` will become `func` in Chrome 93+
          });
          chrome.tabs.onUpdated.removeListener(retryOnTabUpdate);
          return;
        } catch (e) {}
      }
      chrome.tabs.onUpdated.addListener(retryOnTabUpdate);
    }
    
    async function retryOnTabUpdate(tabId, info, tab) {
      if (info.url && /^(file|https?):/.test(info.url)) {
        keepAlive();
      }
    }
    

2. "Forever", via a dedicated tab, while the tab is open

Open a new tab with an extension page inside e.g. chrome.tabs.create({url: 'bg.html'}).

It'll have the same abilities as the persistent background page of ManifestV2 but a) it's visible and b) not accessible via chrome.extension.getBackgroundPage (which can be replaced with chrome.extension.getViews).

Downsides:

  • consumes more memory,
  • wastes space in the tab strip,
  • distracts the user,
  • when multiple extensions open such a tab, the downsides snowball and become a real PITA.

You can make it a little more bearable for your users by adding info/logs/charts/dashboard to the page and also add a beforeunload listener to prevent the tab from being accidentally closed.

Future of ManifestV3

Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.

Solution 2:

If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:

  1. manifest v3
"permissions": [
    "alarms"
],
  1. service worker background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
  console.log('log for debug')
});

Unfortunately this is not my problem and may be you have different problem too. When i refresh dev extension or stop and run prod extension some time service worker die at all. When i close and open browser worker doesn't run and any listeners inside worker doesn't run it too. It tried register worker manually. Fore example:

// override.html
<!DOCTYPE html>
<html lang="en">

  <head>...<head>
  <body>
    ...
    <script defer src="override.js"></script>
  <body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
  for (let worker of res) {
    console.log(worker)
    if (worker.active.scriptURL.includes('background.js')) {
      return
    }
  }

  navigator.serviceWorker
    .register(chrome.runtime.getURL('background.js'))
    .then((registration) => {
      console.log('Service worker success:', registration)
    }).catch((error) => {
      console.log('Error service:', error)
    })
})

This solution partially helped me but it does not matter because i have to register worker on different tabs. May be somebody know decision. I will pleasure.