When a service worker updates, it doesn't take control of the page right way; it goes into a "waiting" state, waiting to be activated.

Surprisingly, the updated service worker doesn't even take control of the tab after refreshing the page. Google explains:


Even if you only have one tab open to the demo, refreshing the page isn't enough to let the new version take over. This is due to how browser navigations work. When you navigate, the current page doesn't go away until the response headers have been received, and even then the current page may stay if the response has a Content-Disposition header. Because of this overlap, the current service worker is always controlling a client during a refresh.

To get the update, close or navigate away from all tabs using the current service worker. Then, when you navigate to the demo again, you should see the horse [updated content].

This pattern is similar to how Chrome updates. Updates to Chrome download in the background, but don't apply until Chrome restarts. In the mean time, you can continue to use the current version without disruption. However, this is a pain during development, but DevTools has ways to make it easier, which I'll cover later in this article.

This behavior makes sense in the case of multiple tabs. My app is a bundle that needs to be updated atomically; we can't mix and match part of the old bundle and part of the new bundle. (In a native app, atomicity is automatic and guaranteed.)

But in the case of a single tab, which I'll call the "last open tab" of the app, this behavior is not what I want. I want refreshing the last open tab to update my service worker.

(It's hard to imagine that anybody actually wants the old service worker to continue running when the last open tab is refreshed. Google's "navigation overlap" argument sounds to me like a good excuse for an unfortunate bug.)

My app is normally only used in a single tab. In production, I want my users to be able to use the latest code just by refreshing the page, but that won't work: the old service worker will remain in control across refreshes.

I don't want to have to tell users, "to receive updates, be sure to close or navigate away from my app." I want to tell them, "just refresh."

How can I activate my updated service worker when the user refreshes the page?

EDIT: There's one answer I'm aware of that's quick, easy, and wrong: skipWaiting in the service worker's install event. skipWaiting will make the new service worker take effect as soon as the update downloads, while the old page tab is open. That makes the update unsafely nonatomic; it's like replacing a native app bundle while the app's running. That's not OK for me. I need to wait until the user refreshes the page of the last open tab.

  • There's one answer I'm aware of that's quick, easy, and unsafe: skipWaiting in the service worker's install event. I don't want to just randomly replace my service worker as soon as the update has been discovered, in the middle of users' work. I need to wait until the user refreshes the page. – Dan Fabulich Oct 18 '16 at 6:21

There are conceptually two kinds of updates, and it isn't clear from your question which we're talking about, so I'll cover both.

Updating content


  • The text of a blog post
  • The schedule for an event
  • A social media timeline

These will likely be stored in the cache API or IndexedDB. These stores live independently of the service worker - updating the service worker shouldn't delete them, and updating them shouldn't require an update to the service worker

Updating the service worker is the native equivalent of shipping a new binary, you shouldn't need to do that to (for example) update an article.

When you update these caches is entirely up to you, and they aren't updated without you updating them. I cover a number of patterns in the offline cookbook, but this is my favourite one:

  1. Serve page shell, css & js from a cache using the service worker.
  2. Populate page with content from the cache.
  3. Attempt to fetch content from the network.
  4. If the content is fresher than what you have in the cache, update the cache and the page.

In terms of "update the page", you need to do that in a way that isn't disruptive to the user. For chronological lists this is pretty easy, as you just add the new stuff to the bottom/top. If it's updating an article it's a bit trickier, as you don't want to switch text from underneath the user's eyes. In that case it's often easier to show some kind of notification like "Update available - show update" which, when clicked, refreshes the content.

Trained to thrill (perhaps the first ever service worker example) demonstrates how to update a timeline of data.

Updating the "app"

This is the case where you do want to update the service worker. This pattern is used when you're:

  • Updating your page shell
  • Updating JS and/or CSS

If you want the user to get this update in a non-atomic, but fairly safe way, there are patterns for this too.

  1. Detect updated service worker "waiting"
  2. Show notification to user "Update available - update now"
  3. When the user clicks this notification, postmessage to the service worker, asking it to call skipWaiting.
  4. Detect the new service worker becoming "active"
  5. window.location.reload()

Implementing this pattern is part of my service worker Udacity course, and here's the diff of the code before/after.

You can take this further too. For example, you could employ some kind of semver-esque system, so you know the version of the service worker the user currently has. If the update is minor you may decide calling skipWaiting() is totally safe.

  • 2
    I am indeed developing an "app." Is there really no way to skipWaiting on refresh? Implementing my own in app refresh button seems like a hack. – Dan Fabulich Oct 20 '16 at 23:12
  • The two options above don't depend on the type of thing you're building, but the thing you're updating. You'll likely use both patterns in the same app or site. What kind of content are you looking to update? – JaffaTheCake Oct 20 '16 at 23:19
  • I'm developing a game. I want to update the game's code, including JS. – Dan Fabulich Oct 21 '16 at 23:38
  • I'm trying to implement the method for updating the app as described above and in the Udacity course, but I never seem to get the 'controllerchange' event. However, using console logs I was able to see the skipWaiting message post, and I can view the service workers in Chrome's dev tools and see the old one disappear as the new one takes over. Any idea why that would be? Edit: GH commit. – David Millar Mar 31 '17 at 2:49
  • Why do you need to update the ServiceWorker when you change js or css? I know the google workbox guides do it that way, but why can't the service worker just fetch the updated files and replace them in the cache. Then sw would only need update when the sw is really updated. – FINDarkside Apr 26 '18 at 9:03

I wrote a blog post explaining how to handle this. https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68

I also have a working sample on github. https://github.com/dfabulich/service-worker-refresh-sample

There are four approaches:

  1. skipWaiting() on installation. This is dangerous, because your tab can get a mix of old and new content.
  2. skipWaiting() on installation, and refresh all open tabs on controllerchange Better, but your users may be surprised when the tab randomly refreshes.
  3. Refresh all open tabs on controllerchange; on installation, prompt the user to skipWaiting() with an in-app "refresh" button. Even better, but the user has to use the in-app "refresh" button; the browser's refresh button still won't work. This is documented in detail in Google's Udacity course on Service Workers and the Workbox Advanced Guide, as well as the master branch of my sample on Github.
  4. Refresh the last tab if possible. During fetches in navigate mode, count open tabs ("clients"). If there's just one open tab, then skipWaiting() immediately and refresh the only tab by returning a blank response with a Refresh: 0 header. You can see a working example of this in the refresh-last-tab branch of my sample on Github.

Putting it all together...

In the Service Worker:

addEventListener('message', messageEvent => {
  if (messageEvent.data === 'skipWaiting') return skipWaiting();

addEventListener('fetch', event => {
  event.respondWith((async () => {
    if (event.request.mode === "navigate" &&
      event.request.method === "GET" &&
      registration.waiting &&
      (await clients.matchAll()).length < 2
    ) {
      return new Response("", {headers: {"Refresh": "0"}});
    return await caches.match(event.request) ||

In the page:

function listenForWaitingServiceWorker(reg, callback) {
  function awaitStateChange() {
    reg.installing.addEventListener('statechange', function() {
      if (this.state === 'installed') callback(reg);
  if (!reg) return;
  if (reg.waiting) return callback(reg);
  if (reg.installing) awaitStateChange();
  reg.addEventListener('updatefound', awaitStateChange);

// reload once when the new Service Worker starts activating
var refreshing;
  function() {
    if (refreshing) return;
    refreshing = true;
function promptUserToRefresh(reg) {
  // this is just an example
  // don't use window.confirm in real life; it's terrible
  if (window.confirm("New version available! OK to refresh?")) {
listenForWaitingServiceWorker(reg, promptUserToRefresh);
  • I just don't get your second and 3rd solution. you're saying to refresh all open tabs. even if someone checks your sample, your comments are wrong. If I have 5 tabs, and i refreshed one of them which figured out there's new service worker, that tab will show button telling me if i want to refresh. That button won't be shown on other tabs and if clicked yes, only this tab will be refreshed. not the other ones. Why are you saying all open tabs refresh? – Nika Kurashvili Feb 12 '20 at 8:16
  • The third solution is given a thorough demonstration in Google's Udacity course on Service Workers. Give it a try. What you'll find is: if the client side is configured to reload on controllerchange, and you click the in-app refresh button in any tab, the controllerchange event fires on all tabs, so they all refresh immediately. – Dan Fabulich Feb 14 '20 at 0:39
  • @DanFabulich using approach 3, i see the banner even for users visiting the very first time, who don't have any service worker for my site. – Puneet Sabharwal Jun 14 '20 at 5:24

In addition to Dan's answer;

1. Add the skipWaiting event listener in the service worker script.

In my case, a CRA based application I use cra-append-sw to add this snippet to the service worker.

Note: if you are using a recent version of react-scripts, the snippet below is automatically added and you won't need to use cra-append-sw.

self.addEventListener('message', event => {
  if (!event.data) {

  if (event.data === 'SKIP_WAITING') {

2. Update register-service-worker.js

For me, it felt natural to reload the page when the user navigates within the site. In the register-service-worker script I've added the following code:

if (navigator.serviceWorker.controller) {
  // At this point, the old content will have been purged and
  // the fresh content will have been added to the cache.
  // It's the perfect time to display a "New content is
  // available; please refresh." message in your web app.
  console.log('New content is available; please refresh.');

  const pushState = window.history.pushState;

  window.history.pushState = function () {
    // make sure that the user lands on the "next" page
    pushState.apply(window.history, arguments);

    // makes the new service worker active
} else {

Essentially we hook into the native pushState function so we know that the user is navigating to a new page. Although, if you also use filters in your URL for a products page, for example, you may want to prevent reloading the page and wait for a better opportunity.

Next, I'm also using Dan's snippet to reload all tabs when the service worker controller changes. This also will reload the active tab.

  .catch(error => {
    console.error('Error during service worker registration:', error);

navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) {

  refreshing = true;

Your Answer

