Tools
← Back to Blog

The Nightmare of Service Worker Caching on iOS vs Android: A Developer's Diary

2026/1/13

Service Worker Debugging Nightmare

I thought I was done. The new calculator features were ready, the layout looked slick on my iPhone, and the deployment pipeline was green.

But then, the emails started coming in.

"Hey, the text is still overlapping on the Samsung S23."
"The old version is still showing on my Pixel."

I pulled out my drawer of test devices—a tangled mess of cables and screens that looked more like a bomb disposal unit than a dev environment.

Mobile Debugging Chaos

Sure enough, on every Android device I tested, the old, broken layout was stubbornly persisting. It was like a ghost in the machine. No amount of refreshing, hard reloading, or clearing local storage seemed to matter for the average user who just visits the site.

The "Zombie" Service Worker

The culprit, as it often is in these cases, was a generic Service Worker configuration I had copy-pasted months ago and forgotten about.

I dug into the Chrome Remote Debugging tools (which, by the way, is a godsend compared to debugging Safari on iOS). The console was cleaner than I expected, but the Application tab told a different story.

The Service Worker was stuck in a "waiting" state. It had downloaded the new assets, but it wasn't activating. It was waiting for all tabs to be closed before swapping out the old cache. But on mobile, users rarely "close" tabs in the traditional sense; they just swipe away or leave them in the background forever.

I tried to force it with a skip waiting call, but it wasn't reliable.

The Code That Saved Me

I realized I needed a nuclear option. I needed to force the browser to acknowledge that the old cache was garbage and strictly load the new assets.

Here is the exact snippet I added to my main entry point to handle the lifecycle more aggressively:

// A forceful approach to handling Service Worker updates
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.getRegistrations().then(function(registrations) {
    for(let registration of registrations) {
      // If we see an update waiting, we should probably tell the user
      // or just force a reload if it's critical.
      if (registration.waiting) {
        // This is where things get tricky.
        // You can postMessage to the worker to skip waiting.
        registration.waiting.postMessage({type: 'SKIP_WAITING'});
      }
    }
  });
}

But that wasn't enough. The cache names themselves were the issue. I had been using a static cache name like v1. The browser saw "v1", checked its internal map, and said "I got this!".

I had to implement a dynamic versioning system. I ended up appending the build timestamp to the cache name during the build process.

// In the service worker file
const CACHE_NAME = 'my-app-cache-' + new Date().getTime(); // or process.env.BUILD_ID

This forced the browser to treat every new deployment as a completely fresh start.

Why Just ONE Line of Code Matters

It’s funny how we spend hours building complex React components, optimizing CSS grids, and fine-tuning animations, only to be brought to our knees by a single caching policy.

Cache Busting Success

Once I deployed the fix with the new versioning strategy, it was like magic. I watched the logs on my server—suddenly, all those stale Android clients started requesting the new JS bundles. The complaints stopped. The "zombie" was dead.

If you're building a PWA or just using Service Workers for offline capabilities, do yourself a favor: Plan your update strategy first. Don't let your users get stuck in the past.

And if you're ever stuck debugging a similar issue, remember: sometimes you just have to burn the bridge (cache) to cross the river.

Advertisement
Ad Slot footer-banner