Service Worker Caching Strategies, with Code

View this page

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&#x27;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