Skip to content

Service Worker Lifecycle — Install, Activate, and Fetch Explained

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Service Worker Lifecycle. We cover key concepts, practical examples, and best practices to help you master this topic.

The service worker lifecycle has three phases: install (pre-cache resources), activate (clean old caches), and fetch (intercept network requests) — each with specific responsibilities that control how your PWA handles offline and caching.

What You'll Learn

By the end of this tutorial, you will understand the complete service worker lifecycle, how to handle each lifecycle event, and how to manage service worker updates without breaking your PWA.

Why It Matters

The service worker lifecycle determines when your app caches resources, when old caches are cleaned, and how updates are applied. Misunderstanding the lifecycle leads to stale content, failed caching, and user confusion when updates do not appear.

Real-World Use

A news PWA that cached articles during install but never cleaned old caches filled users' storage with 200MB of stale data. Implementing the activate event to prune old caches solved the problem and reduced storage usage by 80%.

Lifecycle Overview

Service Worker Lifecycle
    Registration
         ↓
    ┌─────────────┐
    │  Installing  │ ← Install event fires
    └──────┬──────┘
           ↓ (success)
    ┌─────────────┐
    │  Installed   │ ← Waiting (not yet active)
    └──────┬──────┘
           ↓ (no active worker or skipWaiting())
    ┌─────────────┐
    │  Activating  │ ← Activate event fires
    └──────┬──────┘
           ↓ (success)
    ┌─────────────┐
    │  Activated   │ ← Controlling pages, fetch events fire
    └──────┬──────┘
           ↓ (new version detected)
    ┌─────────────┐
    │  Updating    │ ← New install, old waits
    └──────┬──────┘
           ↓
    ┌─────────────┐
    │  Redundant   │ ← Old worker retired
    └─────────────┘

Think of the service worker lifecycle like a shift change at a Factory. The new worker arrives (install), waits until the current shift finishes (waiting), takes over (activate), and handles all new requests (fetch). The old worker leaves when the new one is fully in control.

Registration

Before the lifecycle begins, you must register the service worker from your web page:

// Register service worker from your main JavaScript file
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(registration => {
                console.log('SW registered:', registration.scope);
                console.log('State:', registration.active ? 'active' : 'installing');
            })
            .catch(error => {
                console.log('SW registration failed:', error);
            });
    });
}

Output:

SW registered: https://example.com/
State: installing

Register the service worker after the page loads to avoid delaying the initial render. The load event ensures the page is fully rendered before downloading the service worker script.

Install Event

The install event fires once when the service worker is first registered or when a new version is detected. This is where you pre-cache critical resources your app needs to work offline.

// sw.js — Install event
const CACHE_NAME = 'my-pwa-v1';
const PRECACHE_URLS = [
    '/',
    '/index.html',
    '/styles/main.css',
    '/scripts/app.js',
    '/images/logo.png',
    '/offline.html'
];

self.addEventListener('install', event => {
    console.log('SW: Install event fired');

    // Pre-cache critical resources
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                console.log('SW: Caching app shell resources');
                return cache.addAll(PRECACHE_URLS);
            })
            .then(() => {
                console.log('SW: Installation complete');
                // Force the waiting service worker to become active
                return self.skipWaiting();
            })
    );
});

Output:

SW: Install event fired
SW: Caching app shell resources
SW: Installation complete

event.waitUntil() tells the browser not to terminate the service worker until the promise resolves. If the promise rejects, the installation fails and the service worker is discarded.

skipWaiting() forces the newly installed service worker to skip the waiting phase and activate immediately. Without it, the new worker waits until all pages using the old worker are closed.

Activate Event

The activate event fires after the service worker is installed and ready to take control. Use this event to clean up old caches and migrate data.

// sw.js — Activate event
self.addEventListener('activate', event => {
    console.log('SW: Activate event fired');

    const currentCacheName = CACHE_NAME;

    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    // Delete old cache versions
                    if (cacheName !== currentCacheName) {
                        console.log('SW: Deleting old cache:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        }).then(() => {
            console.log('SW: Activation complete, claiming clients');
            // Start controlling all open pages immediately
            return self.clients.claim();
        })
    );
});

Output:

SW: Activate event fired
SW: Deleting old cache: my-pwa-v0
SW: Activation complete, claiming clients

self.clients.claim() takes control of all open pages immediately. Without it, pages loaded before the new service worker activates continue using the old worker.

Fetch Event

After activation, the service worker intercepts all network requests from pages it controls:

// sw.js — Fetch event
self.addEventListener('fetch', event => {
    console.log('SW: Fetch intercepted:', event.request.url);

    event.respondWith(
        caches.match(event.request)
            .then(cachedResponse => {
                // Return cached response if available
                if (cachedResponse) {
                    console.log('SW: Serving from cache:', event.request.url);
                    return cachedResponse;
                }

                // Otherwise fetch from network
                console.log('SW: Fetching from network:', event.request.url);
                return fetch(event.request)
                    .then(response => {
                        // Optionally cache the response for future
                        return response;
                    })
                    .catch(error => {
                        console.log('SW: Network request failed:', error);
                        // Return offline fallback
                        return caches.match('/offline.html');
                    });
            })
    );
});

Output:

SW: Fetch intercepted: https://example.com/styles/main.css
SW: Serving from cache: https://example.com/styles/main.css
SW: Fetch intercepted: https://example.com/api/data
SW: Fetching from network: https://example.com/api/data

Update Flow

When you deploy a new service worker with changes, the browser detects the byte difference and runs the new worker through the lifecycle again:

  1. New worker installs (install event fires)
  2. New worker enters waiting state if pages are still controlled by the old worker
  3. When all pages close or skipWaiting() is called, the old worker is retired
  4. New worker activates (activate event fires)
  5. New worker takes control of all pages
// Listen for new service worker installation
navigator.serviceWorker.register('/sw.js').then(registration => {
    // Check if there's a waiting worker (update available)
    if (registration.waiting) {
        console.log('Update available, waiting worker exists');
        // Notify user about update
    }

    // Listen for new worker installations
    registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;
        console.log('New service worker detected');

        newWorker.addEventListener('statechange', () => {
            console.log('Worker state:', newWorker.state);
            if (newWorker.state === 'installed') {
                console.log('New service worker installed');
            }
        });
    });
});

Output:

New service worker detected
Worker state: installing
Worker state: installed
Worker state: activating
Worker state: activated

Common Mistakes

  1. Not calling skipWaiting(). Without skipWaiting, the new service worker waits until all tabs are closed. Users must close and reopen the app to get updates.
  2. Not cleaning old caches during activate. Old caches accumulate indefinitely, filling user storage. Always delete caches from previous versions.
  3. Registering every page load. Register the service worker once. Multiple registration calls are safe (the browser ignores duplicates), but they add unnecessary checks.
  4. Assuming immediate control after install. The service worker does not control the page that registered it until the next page load or after clients.claim().
  5. Forgetting error handling in waitUntil. If the promise passed to waitUntil rejects, the installation fails. Always handle errors with .catch().

Practice Questions

  1. What are the three main lifecycle events in a service worker?
  2. Why should you register the service worker after the window load event?
  3. What does skipWaiting() do and why is it important?
  4. Why should you clean old caches during the activate event?
  5. What happens if the install event promise rejects?

Challenge: Write a service worker that pre-caches 5 URLs during install, cleans old caches during activate, and logs every intercepted fetch URL. Test it on a local page and verify all three lifecycle events fire correctly.

FAQ

Can I have multiple service workers on one page?

No. A page can only be controlled by one service worker at a time. The most recently registered worker with the matching scope takes precedence.

What triggers the service worker update?

The browser checks for updates every 24 hours by default. You can also call registration.update() to trigger a check. If the new worker differs by even one byte, the update lifecycle starts.

Does the service worker persist after the browser closes?

Yes. The service worker and its caches persist on disk. When the browser reopens, the service worker activates and intercepts requests as configured.

Can a service worker access the DOM?

No. Service workers run in a worker context without DOM access. They cannot manipulate the page directly. Use postMessage to communicate between the service worker and the page.

What happens if the service worker script returns a 404?

Registration fails. The browser treats the 404 response as an error and discards the service worker. Ensure your sw.js file is served successfully with a 200 status.

Mini Project

Create a service worker that pre-caches a simple app shell (index.html, style.css, app.js, and a logo image). Implement install, activate (with cache cleanup), and fetch events. Register it from an HTML page and verify using Chrome DevToolsk "DevTools" >}} → Application → Service Workers panel.

What's Next

You understand the lifecycle. Now dive deeper into the install event and learn how to pre-cache resources effectively.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro