<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: INNERLUXES</title>
    <description>The latest articles on DEV Community by INNERLUXES (@innerluxes).</description>
    <link>https://dev.to/innerluxes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4155527%2Fbb21b91e-fc68-4232-b4d7-a605d5dc602a.png</url>
      <title>DEV Community: INNERLUXES</title>
      <link>https://dev.to/innerluxes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/innerluxes"/>
    <language>en</language>
    <item>
      <title>Manifest V3 Service Workers: Why Your Chrome Extension Keeps Losing State</title>
      <dc:creator>INNERLUXES</dc:creator>
      <pubDate>Wed, 28 Oct 2026 21:04:03 +0000</pubDate>
      <link>https://dev.to/innerluxes/manifest-v3-service-workers-why-your-chrome-extension-keeps-losing-state-5g</link>
      <guid>https://dev.to/innerluxes/manifest-v3-service-workers-why-your-chrome-extension-keeps-losing-state-5g</guid>
      <description>&lt;p&gt;The bug report always reads the same way. The extension works. Then the user leaves it alone for a minute, comes back, clicks the toolbar icon, and nothing happens. Or the badge count is wrong. Or the auth token the popup needs has vanished. Reload the extension and everything is fine again, which makes the whole thing look like a ghost.&lt;/p&gt;

&lt;p&gt;It is not a ghost. In Manifest V3 the background page is gone and a service worker took its place, and a service worker is not a process that stays running. Chrome starts it when an event needs it and shuts it down when nothing does. Every piece of state held in a module-scope variable goes with it.&lt;/p&gt;

&lt;p&gt;This post covers the exact lifecycle numbers, the five rules that follow from them, and the reason the bug never reproduces while you are watching it in DevTools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Chrome actually does to your worker
&lt;/h2&gt;

&lt;p&gt;Four behaviours matter, and all four are documented:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Idle for 30 seconds and the worker is terminated.&lt;/strong&gt; Receiving an event or calling an extension API resets that timer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A single event or API call that runs longer than 5 minutes is killed.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;fetch()&lt;/code&gt; whose response takes more than 30 seconds to arrive is killed.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global variables do not survive.&lt;/strong&gt; The next event starts a fresh JavaScript instance with every module global back at its initial value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no &lt;code&gt;chrome.runtime.keepAlive()&lt;/code&gt;. There is no manifest key that asks for a persistent worker. The lifecycle is not a setting, it is the contract.&lt;/p&gt;

&lt;p&gt;Two narrow exceptions are worth knowing. Since Chrome 116, four APIs that put a prompt in front of the user are allowed to run past the five-minute ceiling, because the clock is really the user's: &lt;code&gt;desktopCapture.chooseDesktopMedia()&lt;/code&gt;, &lt;code&gt;identity.launchWebAuthFlow()&lt;/code&gt;, &lt;code&gt;management.uninstall()&lt;/code&gt; and &lt;code&gt;permissions.request()&lt;/code&gt;. Also since Chrome 116, an active WebSocket connection extends the worker's life, and every message sent or received over that socket resets the idle timer.&lt;/p&gt;

&lt;p&gt;Everything below follows from those four behaviours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: register every listener at the top level, synchronously
&lt;/h2&gt;

&lt;p&gt;This is the single most common cause of "my extension responds to nothing until I reload it".&lt;/p&gt;

&lt;p&gt;When an event arrives for a sleeping worker, Chrome boots the worker, runs the script top to bottom, and then dispatches the event. If the listener for that event is not registered by the time the first turn of the event loop finishes, the event is already gone. It is not queued.&lt;/p&gt;

&lt;p&gt;So this breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// background.js — BROKEN&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// registered one microtask too late; the event that woke us is already lost&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onClicked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;openPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this breaks for the same reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// background.js — ALSO BROKEN&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// top-level await&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Top-level &lt;code&gt;await&lt;/code&gt; is legal in a service worker module and it still loses you the wake-up event, because the registration now happens after the initial evaluation.&lt;/p&gt;

&lt;p&gt;The fix is to register first and load state inside the handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// background.js — CORRECT&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onClicked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defaults&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;openPanel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// keep the message channel open for the async reply&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onInstalled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seedDefaults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onStartup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resyncFromServer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in that block earn their place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;return true&lt;/code&gt; from an &lt;code&gt;onMessage&lt;/code&gt; listener is what keeps the channel open for an asynchronous &lt;code&gt;sendResponse&lt;/code&gt;. Without it the port closes the moment the listener returns and the sender's callback gets &lt;code&gt;undefined&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onInstalled&lt;/code&gt; fires on install and on update. It is the right place to write default settings and to call &lt;code&gt;chrome.alarms.create&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onStartup&lt;/code&gt; fires when the browser profile starts. It is the right place to rebuild anything that lives only in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful habit is to keep &lt;code&gt;background.js&lt;/code&gt; as nothing but a list of top-level listener registrations plus imports, and push all the work into other modules. If the file has any conditional or awaited code above a registration, that is the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: module scope is a cache, never a source of truth
&lt;/h2&gt;

&lt;p&gt;A variable at module scope in a service worker is a cache with an eviction policy you do not control. Treat it exactly that way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BROKEN — token is gone 30 seconds after the last event&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The version that survives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authExpiry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authToken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authExpiry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authToken&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;authExpiry&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authExpiry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresIn&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the storage area deliberately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secrets and session state&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chrome.storage.session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;In memory, cleared when the browser closes, not written to disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settings that must persist&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chrome.storage.local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Survives restarts and updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settings that follow the user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chrome.storage.sync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Small quotas, per-item size limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large structured data&lt;/td&gt;
&lt;td&gt;IndexedDB&lt;/td&gt;
&lt;td&gt;Use when a quota or a query pattern rules out &lt;code&gt;storage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fetched assets&lt;/td&gt;
&lt;td&gt;CacheStorage&lt;/td&gt;
&lt;td&gt;Same API as in a page worker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt; are not available in an extension service worker at all, which is its own source of confusion when porting a Manifest V2 background page.&lt;/p&gt;

&lt;p&gt;One more trap: &lt;code&gt;chrome.storage&lt;/code&gt; reads and writes are asynchronous, and two events can wake the worker close together. If a counter matters, do not read, add one and write. Serialise the update behind a promise chain, or keep the counter somewhere that gives you an atomic operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: &lt;code&gt;setTimeout&lt;/code&gt; and &lt;code&gt;setInterval&lt;/code&gt; are not timers any more
&lt;/h2&gt;

&lt;p&gt;A timer longer than the idle window simply does not fire, because the worker holding it is gone before the callback is due.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BROKEN — the worker is terminated long before this runs&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;syncWithServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;chrome.alarms&lt;/code&gt;, which lives in the browser rather than in the worker and wakes the worker up to deliver the event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onInstalled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;periodInMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onAlarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncWithServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes that save time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The minimum period is 30 seconds as of Chrome 120. Earlier versions clamp to one minute, and in a released extension anything under a minute was silently rounded up.&lt;/li&gt;
&lt;li&gt;Alarms persist across worker restarts but not across extension updates reliably, so create them in &lt;code&gt;onInstalled&lt;/code&gt; rather than assuming they are still there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chrome.alarms.create&lt;/code&gt; with the same name replaces the existing alarm instead of adding a second one, which makes it safe to call on every install and update.&lt;/li&gt;
&lt;li&gt;Short &lt;code&gt;setTimeout&lt;/code&gt; calls inside a single event handler are fine. The rule is about anything that needs to outlive the current event.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule 4: design for a 5-minute ceiling
&lt;/h2&gt;

&lt;p&gt;A long import, a bulk upload, a full re-index: any one of these can exceed five minutes in one event handler and be killed mid-way, leaving half-written state behind.&lt;/p&gt;

&lt;p&gt;The pattern that holds up is a queue plus an alarm. Write the work items to storage, process a bounded batch per wake-up, and let the alarm bring the worker back for the next batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BATCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onAlarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drain-queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drain-queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BATCH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// returns the ids it finished&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because each wake-up re-reads the queue and removes only what finished, a termination at any point costs one batch of work, not the job. The same shape makes the job resumable after a browser restart for free.&lt;/p&gt;

&lt;p&gt;The 30-second &lt;code&gt;fetch()&lt;/code&gt; limit deserves its own line. If a request to your own backend can legitimately take longer than that, the extension is the wrong place to wait for it. Return a job id immediately and poll.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 5: ports and sockets, and what they actually keep alive
&lt;/h2&gt;

&lt;p&gt;A long-lived connection from a content script or popup via &lt;code&gt;chrome.runtime.connect()&lt;/code&gt; keeps the worker alive while the port is open, and messages across it reset the idle timer. That is useful and it is also a trap, because the port closes when the popup closes or the tab navigates, and the worker then goes to sleep on the usual 30-second clock. Any code that assumed the port is still there needs an &lt;code&gt;onDisconnect&lt;/code&gt; handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onConnect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onTick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onDisconnect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onTick&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the content script side, reconnect rather than assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;panel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onDisconnect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the worker was asleep; try again&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An open WebSocket extends the worker's lifetime since Chrome 116, which makes a push connection viable, but it does not make it permanent. Treat every reconnect as normal operation and keep the server side idempotent.&lt;/p&gt;

&lt;p&gt;What does not work is a keepalive loop that pings itself to stay awake. It burns battery, it is a review flag, and Chrome has closed off the versions of the trick that used to work. If the design needs a process that never sleeps, that process belongs on a server, not in a browser extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 6: anything that needs a DOM goes in an offscreen document
&lt;/h2&gt;

&lt;p&gt;A service worker has no &lt;code&gt;document&lt;/code&gt;, no &lt;code&gt;DOMParser&lt;/code&gt;, no &lt;code&gt;Audio&lt;/code&gt;, no canvas, and no clipboard. Code ported from a Manifest V2 background page hits this within minutes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;chrome.offscreen&lt;/code&gt; API creates a hidden document for exactly these cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ensureOffscreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContexts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;contextTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OFFSCREEN_DOCUMENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offscreen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offscreen.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOM_PARSER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;justification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Parse HTML returned by the sync endpoint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to keep in mind. The &lt;code&gt;reasons&lt;/code&gt; array is reviewed, so it has to be honest. And the offscreen document has its own lifetime, so close it when the work is done rather than leaving it open as a back door to a persistent background page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works on your machine
&lt;/h2&gt;

&lt;p&gt;This is the part that wastes the most days.&lt;/p&gt;

&lt;p&gt;While DevTools is attached to the service worker, Chrome does not terminate it. The worker you are inspecting is immortal, so none of the bugs above can reproduce. The state stays in memory, the interval keeps firing, the token is still there, and the conclusion is that the report must be wrong.&lt;/p&gt;

&lt;p&gt;Reproduce it properly instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;chrome://extensions&lt;/code&gt;, find the extension, and close any DevTools window attached to the worker.&lt;/li&gt;
&lt;li&gt;Open the card's &lt;strong&gt;service worker&lt;/strong&gt; link only when you need the console, and remember that opening it changes the behaviour you are testing.&lt;/li&gt;
&lt;li&gt;On the extensions page, the worker's status shows when it is inactive. Click &lt;strong&gt;Service worker&lt;/strong&gt; and it starts; wait 30 seconds without touching the extension and it stops.&lt;/li&gt;
&lt;li&gt;Exercise the feature cold: trigger the event with the worker already stopped, not immediately after a reload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A faster loop is to make the assumption testable in code. A single accessor for every piece of cross-event state, with a log line on a cache miss, turns "it loses the token sometimes" into a line in the console the first time the worker restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a Manifest V3 worker, confirm each of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every &lt;code&gt;addListener&lt;/code&gt; call runs during initial evaluation, with no &lt;code&gt;await&lt;/code&gt;, condition or callback above it.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;await&lt;/code&gt; at the top level of the worker entry file.&lt;/li&gt;
&lt;li&gt;No cross-event state in module scope that is not backed by &lt;code&gt;chrome.storage&lt;/code&gt;, IndexedDB or CacheStorage.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;setInterval&lt;/code&gt;, and no &lt;code&gt;setTimeout&lt;/code&gt; expected to outlive the current event. Alarms instead, created in &lt;code&gt;onInstalled&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every alarm handler checks &lt;code&gt;alarm.name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every long job is a queue drained in bounded batches, resumable after a kill at any point.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;fetch()&lt;/code&gt; on the critical path that can exceed 30 seconds.&lt;/li&gt;
&lt;li&gt;Every port has an &lt;code&gt;onDisconnect&lt;/code&gt; handler on both sides, and the content script reconnects.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onMessage&lt;/code&gt; listeners that reply asynchronously &lt;code&gt;return true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;DOM work is in an offscreen document that gets closed.&lt;/li&gt;
&lt;li&gt;The feature has been tested once with the worker cold and DevTools closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most Manifest V3 migrations that are described as unstable are not fighting a Chrome bug. They are running Manifest V2 assumptions about a page that never went away on a worker that is designed to. Once state lives in storage, timers live in alarms and listeners are registered in the first turn of the event loop, the worker being terminated stops being an event worth handling.&lt;/p&gt;




&lt;p&gt;Written by the engineering team at Innerluxes. We build Chromium browsers and browser extensions, so this checklist comes out of migrations and audits rather than from the spec alone. If there is a lifecycle case here that does not match what you are seeing, the comments are a good place for it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>chrome</category>
      <category>programming</category>
    </item>
    <item>
      <title>Mobile Health App Development: Architecture and Security Basics for Developers</title>
      <dc:creator>INNERLUXES</dc:creator>
      <pubDate>Thu, 01 Oct 2026 17:57:49 +0000</pubDate>
      <link>https://dev.to/innerluxes/mobile-health-app-development-architecture-and-security-basics-for-developers-1j65</link>
      <guid>https://dev.to/innerluxes/mobile-health-app-development-architecture-and-security-basics-for-developers-1j65</guid>
      <description>&lt;p&gt;Mobile health (mHealth) apps are one of the fastest-growing areas in software. Patients want to book visits, track vitals, and message doctors from their phones. But building a health app is different from building a typical consumer app, because the data you handle is highly sensitive.&lt;/p&gt;

&lt;p&gt;This post covers the architecture and security basics we think every developer should know before starting &lt;strong&gt;mobile health app development&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes Health Apps Different?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sensitive data:&lt;/strong&gt; diagnoses, lab results, and prescriptions need strong protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulation:&lt;/strong&gt; depending on your market, you may need to follow HIPAA, GDPR, or local laws.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust:&lt;/strong&gt; one data leak can end a product and hurt real people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; users depend on reminders, records, and alerts working correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Simple, Safe Architecture
&lt;/h2&gt;

&lt;p&gt;A pattern that works well for most projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mobile App  -&amp;gt;  API Gateway  -&amp;gt;  Backend Services  -&amp;gt;  Encrypted Database
                    |
              Auth + Audit Logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app:&lt;/strong&gt; UI only. Keep as little sensitive data on the device as possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API gateway:&lt;/strong&gt; handles authentication, rate limiting, and request validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend services:&lt;/strong&gt; business logic, appointments, messaging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; encrypted at rest, with strict access control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs:&lt;/strong&gt; record who accessed which record and when.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping logic and data on the server makes it easier to secure, update, and audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use HTTPS (TLS) everywhere.&lt;/strong&gt; Never send health data over plain HTTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add multi-factor authentication&lt;/strong&gt; for patients and staff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply role-based access control.&lt;/strong&gt; A receptionist should not see clinical notes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep PHI out of logs, analytics, and push notifications.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store tokens in the platform keystore,&lt;/strong&gt; not in plain text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set session timeouts&lt;/strong&gt; and auto-logout on inactivity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test regularly&lt;/strong&gt; with security scans and penetration tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Example: Storing a Token Securely in Flutter
&lt;/h2&gt;

&lt;p&gt;Do not keep access tokens in &lt;code&gt;SharedPreferences&lt;/code&gt;. Use secure storage, which relies on the iOS Keychain and Android Keystore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_secure_storage/flutter_secure_storage.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FlutterSecureStorage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;saveToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="s"&gt;'access_token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;value:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;readToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="s"&gt;'access_token'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clearToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="s"&gt;'access_token'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always clear the token on logout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features to Build First
&lt;/h2&gt;

&lt;p&gt;For version 1, keep the scope small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure sign-up and login&lt;/li&gt;
&lt;li&gt;Patient profile&lt;/li&gt;
&lt;li&gt;Appointment booking and reminders&lt;/li&gt;
&lt;li&gt;Secure messaging&lt;/li&gt;
&lt;li&gt;Simple provider dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add wearable sync, analytics, or AI features after you collect feedback from real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treating compliance as a final-week task&lt;/li&gt;
&lt;li&gt;Sending patient names or details in push notifications&lt;/li&gt;
&lt;li&gt;Using third-party SDKs that collect data you did not plan for&lt;/li&gt;
&lt;li&gt;Skipping accessibility, even though many patients are elderly&lt;/li&gt;
&lt;li&gt;No plan for updates after launch&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Native or Cross-Platform?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Native (Swift, Kotlin)&lt;/td&gt;
&lt;td&gt;Deep device features, best performance&lt;/td&gt;
&lt;td&gt;Two codebases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter / React Native&lt;/td&gt;
&lt;td&gt;Faster delivery, one codebase&lt;/td&gt;
&lt;td&gt;Some features need native code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most health apps, cross-platform is a solid starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Good mobile health app development comes down to a simple idea: protect user data first, keep the first release focused, and improve with real feedback.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We are the team at Innerluxes, and we work on &lt;a href="https://innerluxes.dev/healthcare/mobile" rel="noopener noreferrer"&gt;mobile health app development&lt;/a&gt; for clinics and startups. If you have questions about architecture or security, drop them in the comments and I will be happy to help.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is for general information only and is not legal or compliance advice. Check the regulations that apply to your market.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>healthcare</category>
      <category>mobile</category>
      <category>security</category>
      <category>flutter</category>
    </item>
  </channel>
</rss>
