Events that drive a service worker
A service worker is event-driven: it has no persistent state and wakes only to handle events, then may be terminated. Knowing which events exist, when they fire, and how to extend their lifetime is the core of building reliable offline-capable and push-enabled web apps. This searchable reference lists the lifecycle and functional events with their interfaces and timing.
How it works
Service worker events fall into two groups. Lifecycle events run once per worker version:
self.addEventListener("install", (event) => {
event.waitUntil(caches.open("v1").then((c) => c.addAll(ASSETS)));
});
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
});
Functional events fire repeatedly. The fetch event uses
event.respondWith() to return a response synchronously, while push, sync,
message and the notification events use event.waitUntil() to keep the worker
alive while a promise resolves. Without one of these calls the browser may kill
the worker before your asynchronous work finishes.
The install → activate lifecycle in detail
When you deploy a new service worker, the browser runs the install handler
of the new version while the old version is still controlling open pages. The
new worker enters a “waiting” state until all pages using the old version are
closed or the old worker is explicitly skipped with self.skipWaiting().
activate then fires once the new worker takes control. This is the right
place to clean up caches from the previous version — compare the current cache
name against a known list and delete anything obsolete:
self.addEventListener("activate", (event) => {
const currentCaches = ["v2"];
event.waitUntil(
caches.keys().then((cacheNames) =>
Promise.all(
cacheNames
.filter((name) => !currentCaches.includes(name))
.map((name) => caches.delete(name))
)
).then(() => clients.claim())
);
});
Fetch caching strategies
The fetch event is where most service worker logic lives. Common strategies:
Cache-first (for static assets): check the cache first; fall back to network and cache the response. Use this for images, fonts, and versioned JS/CSS bundles.
Network-first (for API calls): try the network; fall back to cache if offline. Use for data that changes but should degrade gracefully.
Stale-while-revalidate: serve from cache immediately (for speed), then fetch fresh content in the background and update the cache for next time.
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
const networkFetch = fetch(event.request).then((response) => {
const clone = response.clone();
caches.open("v2").then((cache) => cache.put(event.request, clone));
return response;
});
return cached || networkFetch;
})
);
});
Push and notification events
The push event fires when the browser’s push service delivers a message, even
when no page is open. Your handler has a short time limit (typically a few seconds)
and must show a notification inside waitUntil(). If it resolves without showing
one, the browser shows a generic notification and may penalise your origin:
self.addEventListener("push", (event) => {
const data = event.data?.json() ?? { title: "New message" };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icon.png",
})
);
});
notificationclick fires when a user taps the notification — use it to open
or focus the relevant page.
Tips and notes
- Precache in
install; purge old caches inactivate. Callclients.claim()to take control of already-open pages immediately. fetchis the only event that usesrespondWith()— everything else useswaitUntil().- A
pushhandler must show a notification or the browser may flag your origin for silent pushes. syncretries on a backoff if your promise rejects;periodicsyncis throttled by engagement scores and battery state and has limited browser support.- Test service workers in a private window to avoid unexpected caching from a previous registered worker.