I Broke Production: The PWA Caching Bug That Haunted Me
"It works on my machine."
I hate that phrase. But last week, I lived it. I had just deployed a critical fix to the Clay Shrinkage Calculator—users were complaining that the percentage slider was jumpy on mobile. I pushed the code, verified it on my laptop, and went to bed.
The next morning, I woke up to emails.
"The calculator is still broken." "I tried refreshing, but nothing changed."
I opened the site on my iPhone. It was fine. I refreshed. Still fine.
Then I opened it on my partner's iPad. Broken. The old code was still running.
The Ghost in the Machine
I checked the console. And there it was, the error that would cost me my weekend:
An unknown error occurred when fetching the script.
ServiceWorker registration failed: TypeError: Failed to register a ServiceWorker...
But wait, why was it failing only on some devices?
The Culprit: next-pwa and Aggressive Caching

I was using next-pwa to make the site offline-capable. Great for users in bad connectivity spots (like pottery studios), but terrible for updates if you mess up the configuration.
In my next.config.js, I had this:
const withPWA = require('next-pwa')({
dest: 'public',
disable: process.env.NODE_ENV === 'development',
// register: true,
// skipWaiting: true,
})
I had commented out skipWaiting: true because I read somewhere it could cause issues. Big mistake.
Without skipWaiting, the new Service Worker enters a "waiting" state. It downloads, but it doesn't take over until the old Service Worker is completely stopped.
On desktop, closing the tab manages this. On iOS Safari? The OS keeps that process alive aggressively. My users were stuck with the old version forever unless they manually cleared their browser data.
The Fix: Brutal Force
I tried being nice. I tried bumping the version number in package.json. Nothing.
Finally, I had to go nuclear. I wrote a script to force-unregister the existing workers on the client side if the version didn't match.
// If you're reading this in my source code, I'm sorry.
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(registrations) {
for(let registration of registrations) {
// Force update!
registration.update();
}
});
}
}, []);

It wasn't elegant. It caused a double-reload for some users. But it cleared the cache.
Lessons Learned
- Test on Real Devices: Chrome DevTools "Device Mode" lies to you. It doesn't emulate the aggressive caching of iOS Safari.
- Versioning Matters: I now append a query string to my sw.js registration:
/sw.js?v=20260124. It forces the browser to treat it as a new file. - Logs Are Life: If I hadn't wired up Sentry for frontend errors, I would have assumed those emailed complaints were just user error.
If you're building a PWA in 2026, do yourself a favor: turn on skipWaiting: true unless you have a really, really good reason not to.
Now, if you'll excuse me, I have to go check my server logs. just to be safe.
