A service worker without an offline promise

M31V cached articles enthusiastically, then removed the service worker when it could not explain freshness, coverage, updates, or recovery to readers.

I added a service worker to M31V because the capability felt like the future arriving early. The publication was mostly static, its articles were cacheable, and reading offline sounded obviously good.

The prototype cached the home page, shared styles, and every article a reader opened. On a disconnected laptop, previously visited pages loaded. The demo was satisfying.

Then an old article remained visible after a correction, the home page listed a new story that was not cached, and a long-lived tab continued under an earlier worker while another tab used the new one.

I had built a cache strategy without defining an offline product.

M31V removed the service worker until it could make a precise promise worth the state complexity.

“Works offline” concealed several products

The phrase could mean:

  • The publication shell opens without a network.
  • Every article previously visited opens.
  • A deliberately saved reading list opens.
  • The latest home page and its linked articles open together.
  • The entire archive is available offline.
  • Navigation never shows the browser's offline error.

The prototype provided parts of the second and sixth, inconsistently. A cached home page could imply that linked articles were available when they were not. A cached article could appear current without saying when it had been stored.

I wrote a reader task before choosing a cache rule: “Save this article for later and know whether the saved copy is available, when it was saved, and how to remove or refresh it.”

That task required explicit UI and storage policy the experiment did not have. Opportunistic caching alone did not satisfy it.

Offline capability needed scope, evidence, and reader control.

The first strategy was cache first forever

The fetch handler looked roughly like this:

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request).then(function (cached) {
      return cached || fetch(event.request).then(storeAndReturn);
    })
  );
});

Once a response entered the cache, the network path never checked it again. Fingerprinted styles were safe under that model. HTML lived at stable URLs and could change.

An article correction deployed successfully, public verification passed, and returning readers still received the old cached HTML. The response had no visible age or update path.

Changing to network first improved freshness while connected and made slow or flaky connections wait before falling back. Stale-while-revalidate returned quickly and introduced a period where one view showed old content while an update happened silently.

Each strategy expressed product policy. None was “the service worker default.”

Cache names were release state

The prototype used one cache name. A new worker added new assets to it and removed files no longer in the current manifest. An old active worker could still expect those files.

I versioned caches by release identity and installed a complete core set before considering the worker ready. Activation removed old caches only when the new worker controlled appropriate clients.

That solved one class of mixed assets and exposed the worker lifecycle. A newly downloaded worker could wait while pages remained controlled by the old worker. Forcing immediate activation could replace behavior beneath an open page built against the previous asset contract.

M31V's ordinary artifact activation switched the origin pointer cleanly. A service worker added client-side release state with its own installation, waiting, activation, and control boundaries.

The browser could legitimately contain several publication moments at once: open page, controlling worker, installed waiting worker, HTTP cache, and Cache Storage entries.

Offline was now a release system inside every reader's browser.

Pre-caching a core set during installation could fail if one required resource was unavailable. That was useful when the set truly formed an atomic offline shell.

M31V did not have an application shell separate from content in a meaningful reader task. The article HTML itself was the product. Caching only navigation and styles could produce a polished offline page with nothing to read.

Caching the current home and all linked articles made installation heavier and tied the worker to an arbitrary publication slice. A large image failure could prevent the whole new worker from installing. Omitting images could make saved articles incomplete.

I could choose a smaller set technically. I could not explain why that set deserved an offline guarantee.

The install manifest forced a product inventory: which resources must exist together for offline use to be honest? The prototype had no defensible answer beyond “these were easy to list.”

Runtime caching made coverage invisible

Caching every successful article visit seemed adaptive. The reader had no indication which pages were now available offline. Browser storage could later evict entries, private modes could behave differently, and a failed cache write might not affect the online response.

The site could not promise that “visited” meant “saved.” A response appearing on screen proved network or cache delivery, not durable offline storage.

A trustworthy saved-reading feature would need:

explicit save action
confirmed cache write for article and required assets
visible saved state and timestamp
storage-failure explanation
remove action
refresh behavior
inventory of saved items

That began to resemble X8B6's distinction between optimistic UI and durable local intent. Cache Storage was not a product state merely because a fetch handler wrote to it.

The experiment offered accidental coverage and a confident offline slogan. I removed the slogan first.

A common pattern returned a cached offline page when navigation failed. M31V's first fallback looked like a generic article shell with “You are offline.” It was better than the browser error and could conceal which requested content was unavailable.

For a missing article, the fallback needed to retain the requested title only if the system actually knew it, distinguish never-saved from evicted, and offer saved alternatives. Otherwise it was a branded dead end.

I tested returning the cached home page for every navigation. That was worse: the address bar showed one article while the body displayed another page. It converted network failure into incorrect routing.

An honest fallback said that the requested page was not stored and listed genuinely available saved items. The prototype did not maintain that inventory reliably.

Progressive enhancement sometimes means allowing the browser's accurate failure instead of replacing it with an attractive fiction.

Cache lookup by URL alone ignored request method, headers, content negotiation, credentials, and response variation. M31V's public article GETs were simple, but preview, feeds, and dynamic forms were not all safe to intercept alike.

The worker narrowed its scope to same-origin, public, cacheable GET requests under declared content paths. It bypassed newsletter actions, authenticated preview, diagnostics, and requests with unsupported variation.

It checked response status and type before caching. A server error page should not become the durable article for a canonical URL. Redirected responses required deliberate handling so the cache did not obscure URL migration.

This boundary code grew because fetch interception sits under every request in scope. A broad handler could change behavior far beyond the offline demo.

Service workers were powerful precisely where mistakes became systemic.

Redirects and caches carried different history

M31V had just made redirects part of its content graph. A cached old article at /articles/builds/ could prevent the request from reaching the new redirect to /writing/reliable-builds/.

The worker needed to respect canonical identity. Historical paths should not remain independent cached documents after migration. A fetch could follow the network redirect and store under the right relationship, but offline requests to the old path still needed a policy.

The cache manifest could include alias metadata mapping old path to saved canonical content. That added another copy of the redirect graph and an update obligation inside the browser.

Deleting old cached paths during activation assumed the new worker activated. Long-lived clients could retain the old relationship.

The feature now interacted with the publication's strongest continuity promise. “Cache pages” was not isolated from URL architecture.

Stale-while-revalidate could serve a cached article and fetch a new one in the background. If the update succeeded, should the open page refresh, show a notice, or remain stable until next visit?

Automatic replacement could move text while someone read. Silent background update meant another tab might show different content. An “updated version available” notice required comparing meaningful article identity and release version, not every byte changed by a template.

M31V's build manifest had content IDs and release identity. The worker could use them, but the browser page and worker needed a communication protocol. A waiting worker further complicated which code understood which manifest shape.

This was solvable. It was also substantial product work for a publication whose network path already loaded small HTML reliably.

The experiment had started from capability and was now inventing update UX to justify it.

Storage pressure had product consequences

Cache Storage is not an infinite archive. The browser can enforce quotas and may reclaim data under pressure. M31V could not equate a successful cache write today with permanent availability.

An explicit saved-reading feature could monitor storage results, show what remained saved, and let readers remove large items. It would still document that browser storage was device-local and subject to browser policy.

Runtime caching of every image and article had no budget. A long reading session could accumulate large assets whose offline value was unclear. LRU-style eviction would require tracking access and deciding whether implicit caches or explicit saved items had priority.

The sensible policy favored a small core and user-selected content. The current UI had neither concept.

Storage cost turned opportunistic caching into an unowned retention system.

M31V could roll the origin pointer from build b071 to b070. A client controlled by the b071 worker might continue serving b071 cached HTML and assets after rollback.

The deployment system needed to know which service-worker version a release registered and whether the previous release remained compatible with clients that had already installed the newer worker. A broken worker can outlive a server rollback because browsers update on their lifecycle.

An emergency recovery worker could stop interception and clear known caches, but it still had to install and activate on returning clients. Readers offline during the incident would remain under the old behavior.

This dramatically widened the meaning of rollback. The origin artifact was no longer the only active release state.

For a feature with unclear reader value, that recovery burden was difficult to justify.

Secure context was the easy requirement

Service workers required the security properties associated with a secure context, apart from local development allowances. M31V's HTTPS migration made that technically feasible.

HTTPS protected the script from network modification. It did not make the caching policy correct. A securely delivered worker could still serve stale content indefinitely, intercept the wrong requests, or delete saved items during activation.

This paralleled the HTTP/2 lesson. Infrastructure could enable a capability without supplying its product contract.

I kept worker registration out of the public artifact until HTTPS, scope, update, and removal behavior could be tested together. A partial rollout under one path used a deliberately narrow scope.

Meeting the platform prerequisite was the beginning of responsibility, not completion.

Registering the worker under a narrow path reduced blast radius. I tested it on an experimental /labs/offline/ section with invented articles.

The scope limited which requests the worker controlled. It did not tell readers what was saved, whether an article was current, or how to recover from a broken update. A small vague promise remained vague.

The experiment did prove lifecycle and cache mechanics safely. It supplied fixtures for install failure, old-worker control, offline navigation, and cache cleanup without risking the publication root.

This was useful research, not a reason to expand scope. Successful sandbox behavior answered “can we implement it?” The product still had to answer “what are we promising?”

I treated the lab as an abandoned prototype with documented findings rather than a feature waiting to escape.

Tests needed several browser moments

A single fresh-browser test missed most worker failures. The fixture matrix included:

first visit before registration controls the page
worker installs and controls later navigation
new worker waits while an old tab remains open
old cache exists during new install
network disappears between HTML and image
origin rolls back after new worker activation
storage write fails
old canonical path redirects in a newer release
reader returns after several releases

The test asserted response source—network, HTTP cache, or service-worker cache—along with visible content and worker version. Developer tools made some of that observable; a small diagnostic page reported controlled state without exposing it as reader-facing complexity.

The scenarios were expensive compared with static artifact checks. That cost was legitimate if offline reading became core. It was disproportionate for an implicit cache experiment.

Testing effort helped reveal the feature's true operational size.

Deleting the registration code did not immediately remove already installed workers or caches. M31V shipped a cleanup version for the experiment scope.

The cleanup worker activated, removed caches it owned, stopped intercepting requests, and unregistered where appropriate. Cache names carried a project prefix so cleanup did not delete storage owned by another feature.

The origin continued to serve network pages correctly throughout. A diagnostic route helped confirm that returning clients left the experiment.

I retained the cleanup behavior long enough for likely returning clients and documented that some dormant browser profiles could keep old state longer.

Feature removal in a client-controlled runtime has a tail. “We deleted the file” was as incomplete as deleting a server migration after deployment.

The failed experiment taught lifecycle most clearly while leaving.

M31V's articles were small, cacheable documents. Ordinary HTTP caches made repeat visits efficient. Fingerprinted assets could remain cached for long periods, while HTML revalidated and received corrections.

The publication did not have an established demand for deliberate offline libraries. Improving image selection, route cache policy, and resilient HTML offered value to every reader without installing a programmable network intermediary.

This comparison mattered. The service worker was not competing with “no caching.” It was competing with a mature browser and HTTP cache model that already served the core reading path.

The programmable layer was worthwhile only for behavior the ordinary cache could not express and readers could understand. The prototype had not identified that behavior strongly enough.

Novel capability carried a higher proof burden than incremental infrastructure.

I wrote the conditions under which M31V should revisit offline reading:

  • Readers demonstrate a need to save articles deliberately.
  • The interface can show saved status, age, size, and removal.
  • Article and required-asset manifests form an atomic saved unit.
  • Updates preserve reading stability and disclose newer versions.
  • Storage failure and eviction are visible.
  • URL migrations preserve saved canonical identity.
  • Worker rollback and cleanup are tested across releases.
  • The feature has a bounded storage budget and retention policy.

That list turned an abandoned experiment into a design brief rather than an anti-service-worker opinion.

The platform was capable. M31V had not yet earned a product that used it responsibly.

Removing it was the successful outcome

The service worker demo made a previously visited page open offline. It also created a second release lifecycle, invisible coverage, uncertain freshness, storage ownership, redirect interaction, and rollback behavior that the reader could not inspect.

I could have kept refining until the cache bugs became less visible. Removing the worker preserved a simpler, accurate contract: the publication used ordinary HTTP caching and required a connection for pages not already handled by the browser's normal cache.

The experiment changes the standard I will use for later work. Local storage is not an offline product, a successful write needs visible ownership, and client-side release state survives server rollback.

The failure was beginning from “what can this API do?” instead of “which interruption should this product handle?”

A service worker is a programmable network boundary with a long memory. M31V did not need one until it had a promise precise enough to deserve both.

I expect installable web applications to make this boundary look deceptively ordinary. A badge or home-screen icon may encourage teams to call a site “offline” before they have named which work survives, how old it may be, or who owns an update conflict. The durable product primitive will not be the worker itself. It will be a visible saved unit with identity, version, size, freshness, and a removal path.