Service Worker Caching Strategies, with Code
Cache first, network first and stale-while-revalidate explained with a complete service worker file, update rules, storage limits and what never to cache.
- Author
- WavX Editorial Team
- Published
- 2026-10-02T09:00:00.000Z
- Updated
- 2026-10-02T09:00:00.000Z
- Organisation
- WavX Solutions
- Telephone
- +919310079927
Description
All articles PWA Web Development Software Architecture Software Development
Service Workers and Caching Strategies, Explained with Code
WavX Editorial Team Engineering & delivery team, WavX Solutions
Published 2 October 2026 11 min read 2,577 words
Custom software, built from scratch · Building since 2022 · Gurgaon, Delhi NCR
Part of our Software Development guide Custom Software Development Company Summarise with AI ChatGPT Claude Perplexity Google AI
A service worker is a JavaScript file the browser runs in the background, separate from your pages, that can intercept every network request those pages make. A caching strategy is the rule it applies to one kind of request: answer from a cache, go to the network, or combine the two. Most sites need three rules: cache first for versioned CSS, JavaScript and fonts; network first for HTML pages, with an offline fallback; and stale-while-revalidate for images. Anything that changes data or involves money goes straight to the network.
The code in this guide is written against the Service Worker and Cache APIs as documented on MDN, web.dev and Chrome for Developers on 2 October 2026. It is an example for this article, not a published, device-tested demo. Run it in the browsers your visitors use before you ship it.
If the term is new, start with the short definition of a service worker, or with what a progressive web app is.
What a service worker is allowed to do
Four rules explain most of the surprises.
HTTPS only. MDN states that service workers are restricted to running across HTTPS. localhost counts as secure so that you can develop locally.
Scope follows the file's location. By default a worker controls only URLs in or below the directory it is served from. A file at /js/sw.js controls /js/ and nothing else. To control the whole site, serve it from the root as /sw.js . A server can widen the scope with the Service-Worker-Allowed response header.
It does not control the page that installs it. On the first visit the page loads without a worker, registers one, and stays uncontrolled until the next load unless the worker calls clients.claim() . Do not judge your caching on a first visit.
It is event-driven. The browser starts the worker when an event such as fetch or push arrives and may stop it afterwards. Treat variables in the worker as temporary and keep anything that must survive in the Cache API or IndexedDB.
The five caching strategies
These names come from Jake Archibald's Offline Cookbook on web.dev and are used the same way in the Chrome and MDN documentation.
Strategy
What it does
How fresh
Works offline
Use it for
Cache only
Answers from the cache and never touches the network
As fresh as the last release
Yes
Files stored at install: the offline page, the app shell
Network only
Passes the request through untouched
Always current
No
POST requests, cart, checkout, payments, analytics pings
Cache first, falling back to network
Looks in the cache; on a miss, fetches and stores
Stale until the file name or cache name changes
Yes, once stored
Hashed CSS, JavaScript and fonts
Network first, falling back to cache
Tries the network; on failure, serves the last stored copy
Current whenever online
Yes, for pages already visited
HTML pages, API reads that should be current
Stale-while-revalidate
Serves the cached copy at once and refreshes the cache in the background
One visit behind
Product images, avatars, content that can be slightly old
MDN's caching guide makes the point that matters most: a single app normally uses several of these, one per type of resource. Choosing one strategy for the whole site is the usual beginner mistake.
A complete service worker
Save this as sw.js at the root of the site. It stores an offline page and two core files at install, removes old caches when a new version activates, and routes each request to one of three strategies.
// sw.js
const VERSION = "v1";
const STATIC_CACHE = `static-${VERSION}`;
const PAGE_CACHE = `pages-${VERSION}`;
const IMAGE_CACHE = `images-${VERSION}`;
const OFFLINE_URL = "/offline.html";
const PRECACHE = [OFFLINE_URL, "/styles/main.css", "/scripts/app.js"];
const NEVER_CACHE = ["/api/", "/cart", "/checkout", "/account"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => cache.addAll(PRECACHE))
);
});
self.addEventListener("activate", (event) => {
const keep = [STATIC_CACHE, PAGE_CACHE, IMAGE_CACHE];
caches.keys().then((keys) =>
Promise.all(
keys.filter((key) => !keep.includes(key)).map((key) => caches.delete(key))
async function putInCache(cacheName, request, response) {
const cache = await caches.open(cacheName);
await cache.put(request, response);
async function cacheFirst(event, cacheName) {
const cached = await caches.match(event.request);
if (cached) return cached;
const response = await fetch(event.request);
if (response.ok) {
event.waitUntil(putInCache(cacheName, event.request, response.clone()));
return response;
async function networkFirst(event, cacheName) {
try {
} catch (error) {
return cached || (await caches.match(OFFLINE_URL)) || Response.error();
async function staleWhileRevalidate(event, cacheName) {
const network = fetch(event.request).then((response) => {
if (cached) {
event.waitUntil(network.catch(() => undefined));
return cached;
return network;
self.addEventListener("fetch", (event) => {
const { request } = event;
const url = new URL(request.url);
// Leave non-GET and cross-origin requests to the browser.
if (request.method !== "GET" || url.origin !== self.location.origin) return;
// Network only: never store cart, checkout, account or API traffic.
if (NEVER_CACHE.some((path) => url.pathname.startsWith(path))) return;
if (request.mode === "navigate") {
event.respondWith(networkFirst(event, PAGE_CACHE));
} else if (["style", "script", "font"].includes(request.destination)) {
event.respondWith(cacheFirst(event, STATIC_CACHE));
} else if (request.destination === "image") {
event.respondWith(staleWhileRevalidate(event, IMAGE_CACHE));
Register it from your pages. The feature check keeps older browsers working as before.
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js").catch((error) => {
console.error("Service worker registration failed:", error);
</script>
You also need a plain /offline.html page that does not depend on anything outside the precache list.
How each part behaves
Install. cache.addAll() is all or nothing. MDN documents that it rejects if any response is outside the 200 range, and when the promise passed to event.waitUntil() rejects, the install fails. One wrong path in PRECACHE therefore means no service worker at all. Keep the list short and check every entry.
Activate. MDN recommends cleaning up old caches in the activate event, because at that point no earlier version of the worker is running. The VERSION constant is what makes this work: change it and every old cache is deleted on activation.
Cache first. Fast, and dangerous with file names that never change. If /styles/main.css is stored under cache first, visitors keep the old file until VERSION changes. Either bump VERSION on every release or, better, use hashed file names such as main.4f9a1c.css , which most build tools produce.
Network first. The right default for HTML. Online visitors always get the current page; offline visitors get the last copy they saw, and the offline page for anything they have not visited. The cost is that a slow connection is still slow, because the cache is consulted only after the network fails.
Stale-while-revalidate. The visitor sees the stored image at once and the worker refreshes the cache behind it, so a changed image appears on the following visit. Do not use it for prices, stock levels or anything a customer acts on.
Three details in the code are easy to get wrong:
response.clone() . A response body can be read once. The clone goes to the cache and the original goes to the page.
response.ok . Without this check a 404 or 500 page gets cached and served again. It also skips opaque cross-origin responses, whose reported status is 0.
event.waitUntil() . It tells the browser the worker still has work to finish after the response has been returned. MDN's own example uses the same pattern for the cache write.
What must never be cached
The early return statements in the fetch handler matter more than the strategies. Requests that fall through are handled by the browser as if no worker existed.
Anything that is not a GET request. MDN notes that caching is never appropriate for requests such as POST.
Cart, checkout and payment pages and the calls behind them.
Login, account and order-history pages. A page cached for one user must never be shown to another on a shared device.
API responses that carry prices, stock, balances or anything else that must be current.
Third-party scripts for payments and analytics. Let them load normally.
On an online store, serving a stale price from a cache is worse than showing an error. The guide to PWAs for e-commerce in India covers the split for a storefront.
How updates reach users
This is the part that catches teams after launch.
The browser checks for a new worker on every navigation to a page in scope, and on events such as push unless it checked in the previous 24 hours. The lifecycle article on web.dev notes that most browsers ignore HTTP caching headers for this check by default; the updateViaCache option on register() controls it.
Any byte of difference counts. If the new sw.js differs at all, it is installed alongside the old one. Changing VERSION is enough.
The new worker waits. It activates only when the old worker controls no open pages. A visitor who keeps a tab open keeps the old version.
You can skip the wait. Calling self.skipWaiting() in the install handler activates the new worker immediately. The risk is that a page loaded with old HTML is suddenly served by new logic and new assets. Skip waiting only if your pages tolerate that, or prompt the visitor to reload.
Whichever you choose, write it down. When someone asks why the old version is still showing after a release, the answer is almost always in this list.
Storage limits and eviction
A cache is borrowed space. As of October 2026, MDN documents these limits:
Browser
Per-origin limit
Notes
Chrome, Edge and other Chromium browsers
Up to 60% of total disk size
Same in best-effort and persistent modes
Firefox
The smaller of 10% of disk or 10 GiB
Up to 50% of disk if persistent storage is granted
Safari, from iOS 17 and macOS 14
Around 60% of total disk for browser apps
Around 15% for other apps that embed web content
When a device runs short of space, browsers delete data for the least recently used origins first. Safari also deletes script-created data, which includes service worker registrations and caches, for a site with no user interaction in seven days of browser use when cross-site tracking prevention is on. navigator.storage.estimate() reports approximate usage and navigator.storage.persist() asks the browser not to evict your origin.
The practical rule: cache only what can be downloaded again, and never treat the cache as the only copy of user data. iPhone specifics are in PWAs on iPhone: what works and what does not .
Two newer options
Navigation preload lets the browser start the network request for a page while the service worker is still starting up, which removes a delay from network-first navigations. MDN's compatibility data lists it from Chrome 59, Firefox 99 and Safari 15.4.
The static routing API ( InstallEvent.addRoutes() ) lets a worker declare routes that the browser can handle without starting the worker at all. The same data lists it from Chrome 123 and Safari 27, released in September 2026, and not in Firefox, so treat it as an optimisation and not something to depend on.
Hand-written or a library
Workbox, described in the Chrome documentation as production-ready service worker libraries and tooling, wraps the same strategies in modules for routing, precaching and expiry. It earns its place when the precache list should be generated by the build, when caches need size or age limits, or when there are many routes. For Next.js projects, the Next.js documentation names Serwist as one option for service-worker-based offline caching.
For a site with three or four rules, a hand-written file like the one above is easier to reason about, and nothing in it is hidden.
Testing before release
Chrome DevTools has what you need in the Application panel:
Service Workers shows the registered worker and its state, with controls to go offline, update on reload, bypass the worker for network, and unregister it.
Cache Storage lists every cache and the responses in it.
Clear storage unregisters workers and wipes caches in one click, so you can repeat a first visit.
Test four things: a first visit, a repeat visit, a repeat visit with the Offline box ticked, and a release with a new VERSION . Then repeat on a real Android phone and a real iPhone.
A kill switch
A faulty service worker keeps serving whatever it cached, to every returning visitor, until it is replaced. Chrome's documentation on removing buggy service workers recommends deploying a replacement worker that has no fetch handler, so that the browser handles every request itself. The file below follows that advice and also deletes the caches and unregisters the worker. Keep it ready.
// sw.js (emergency replacement)
self.addEventListener("install", () => self.skipWaiting());
(async () => {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
await self.registration.unregister();
})()
Deploy it at the same URL as the worker it replaces. The browser picks it up at its next update check, which is why the fix is a new file at that address and not a change anywhere else.
When you do not need a service worker
A service worker adds a second cache with its own rules, and a new class of bug. It is not worth it when:
The site is a brochure or a blog that visitors read once. Correct HTTP cache headers and a CDN give most of the speed benefit with none of the risk.
Nobody will own it. A worker written at launch and never revisited is how sites end up serving a months-old home page.
The real problem is a slow first load. A service worker cannot help a first visit, because it is not in control yet. Fix images, scripts and server response time first.
It is worth it when visitors return often, when connections drop in the middle of a task, or when the site should be installable and usable offline. That is the case for a progressive web app , and it is the work WavX takes on under PWA development , including the caching rules, the update plan and the testing described here.
Sources
MDN: Using Service Workers , read 2 October 2026
MDN: Caching (Progressive web apps guide) , read 2 October 2026
MDN: ServiceWorkerContainer.register() , read 2 October 2026
MDN: Cache.addAll() , read 2 October 2026
MDN: Storage quotas and eviction criteria , read 2 October 2026
MDN browser-compat-data, the data behind MDN's compatibility tables (NavigationPreloadManager, InstallEvent.addRoutes) , read 2 October 2026
web.dev: The Offline Cookbook , read 2 October 2026
web.dev: The service worker lifecycle , read 2 October 2026
Chrome for Developers: Strategies for service worker caching , read 2 October 2026
Chrome for Developers: Workbox , read 2 October 2026
Chrome for Developers: Removing buggy service workers , read 2 October 2026
Chrome for Developers: Debug Progressive Web Apps , read 2 October 2026
WebKit: Full Third-Party Cookie Blocking and More (seven-day cap on script-writable storage) , read 2 October 2026
Next.js documentation: Progressive Web Applications , read 2 October 2026
Frequently asked questions
What is the best caching strategy for a service worker? There is no single best one. Use cache first for versioned static files such as CSS, JavaScript and fonts, network first for HTML pages and API reads that must be current, and stale-while-revalidate for images and content that can be one visit old. Anything that changes data, such as cart, login and payment requests, should go straight to the network.
Does adding a service worker make a website work offline automatically? No. A service worker does only what its code says. If the fetch handler does not store a response in a cache, there is nothing to serve when the network is gone. Offline support has to be designed: which pages are saved, what the visitor sees for pages that are not saved, and what happens to forms submitted without a connection.
Why do visitors still see the old version of my site after I deploy? Usually because a cache-first rule is serving files whose names did not change, or because the new service worker is installed but waiting. By default a new worker activates only when no open tab is still using the old one. Change the cache name or use hashed file names on every release, and decide deliberately whether to call skipWaiting.
Should I write the service worker by hand or use Workbox? For a handful of rules, a hand-written file of under a hundred lines is easy to read and debug. Workbox, the library set documented by the Chrome team, is worth using when you need precaching generated from a build, cache expiry rules or many routes. Either way the strategies are the same.
How much data can a service worker cache? It depends on the browser and the device. As of October 2026 MDN documents up to 60% of total disk size per origin in Chromium browsers, the smaller of 10% of disk or 10 GiB in Firefox by default, and around 60% of disk in Safari from iOS 17 and macOS 14. Browsers can evict that data, so a cache is never a place for anything you cannot fetch again.
About the author
WavX Editorial Team
Engineering & delivery team, WavX Solutions
Written and fact-checked by the WavX Solutions engineering team in Gurgaon, Delhi NCR — the people who scope, price and ship these builds. Costs and timelines quoted here come from projects we have actually delivered, not vendor price lists.
All articles by WavX Editorial Team →
Build your own software — your way, your pricing.
WavX Solutions is here to create your own software in a fully custom way, built exactly how you work — with a pricing model that fits your business. Connect now and let's build it.
Contact Now helpwavx@gmail.com
More on Software Development
Software Development hub
Software Development Best inventory and dispensing system for pharmacy chain the United Kingdom 2026: £30,000–£120,000 Real Pricing
Software Development Custom Inventory Management System Cost for Wholesale Distributor Nigeria 2026: ₦18M–₦45M
Software Development Custom Inventory Management System Cost for Wholesale Distributor Egypt 2026: EGP 2,200,000–EGP 3,200,000 Real Pricing
Software Development Custom Inventory Management Cost Turkey – ₺120k‑₺300k 2026
Software Development Custom Inventory Management System Cost for Wholesale Distributor Mexico 2026: MX$800,000–MX$2,200,000
Software Development Custom Inventory Management System Cost for Wholesale Distributor Oman 2026: OMR 12,000–OMR 28,000
Keep reading
Monolith vs Microservices: Startup Architecture in 2026
Read REST vs GraphQL: Which API Should You Use for Your App?
Read Custom Inventory Management System Cost for Wholesale Distributor Indonesia 2026: Rp30 M–Rp80 M
Read Software Development Hourly Rates in India (2026)
Read Fleet Management Software Development in India: Custom Logistics
Read Society Management Software in India: Features & Cost Guide
Read Custom Software vs SaaS: When to Build and When to Buy
Read Software Development Company Noida vs Gurgaon: 2026 Guide
Read Best Tech Stack for Startups in 2026: How to Choose the Right One
Read How to Hire a Software Development Company in India (2026 Checklist)
Read In-House vs Agency vs Freelancer: The Best Way to Build Your Software
Read How to Build an ERP System: A Practical 2026 Guide
Read