Setting Up a Service Worker in Ruby on Rails

Illustration of a browser window showing a cached offline copy, connected to a blue gear labeled service worker, which links to a crossed-out network server and to a cache box with a green check

Updated September 2026

A service worker is a script the browser runs in the background, separately from your pages. It sits between your app and the network, so it can answer requests itself: serve files from a cache, show an offline page when the connection drops, or receive push notifications. It has no access to the DOM, and it isn't the same thing as a web worker, which runs heavy JavaScript off the main thread for a single page.

We first published this in 2020, when adding a service worker to Rails meant writing your own controller. Since Rails 7.2, new apps come with the pieces already in place. This version covers both.

How a service worker works

Four events do most of the work. Your page registers the worker, the worker installs and caches what it needs, it activates and cleans up after older versions, and from then on it sees every request the page makes.

Service worker lifecycle in a Rails app: the page registers /service-worker.js, the install event caches the offline page, the activate event deletes old caches, and the fetch event tries the network first and falls back to the cached offline page 01 · REGISTER The page asks for it /service-worker.js 02 · INSTALL Cache the essentials The offline page, and anything it needs to show. 03 · ACTIVATE Clear old caches Delete every cache that isn't the current version. 04 · FETCH Network first If a page load fails, serve the cached offline page.
The lifecycle of a simple offline-fallback service worker.

Two rules shape everything else. Service workers only run in a secure context, so you need HTTPS in production (localhost is allowed for development). And a worker can only control pages inside its scope, which by default is the folder the script is served from. Serving it from the site root, as /service-worker.js, lets it cover the whole app.

Rails 7.2 and later: use the built-in PWA files

New Rails apps generate app/views/pwa/service-worker.js and app/views/pwa/manifest.json.erb, plus a Rails::PwaController that renders them. Because they're views, you can use ERB in them. In Rails 8 the routes ship commented out in config/routes.rb; uncomment them:

get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest

If you want the app to be installable, also uncomment the manifest link in app/views/layouts/application.html.erb:

<%= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>

If you're upgrading an older app, you can add the two view files and routes yourself: the controller is part of Rails.

Write the worker

The generated worker is only commented-out examples. Here's a small one that caches an offline page on install, clears old caches on activate, and serves the offline page when a page load fails:

// app/views/pwa/service-worker.js
const CACHE = "offline-v1";
const OFFLINE_URL = "/offline.html";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE).then((cache) => cache.add(OFFLINE_URL))
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.filter((key) => key !== CACHE).map((key) => caches.delete(key))
      )
    )
  );
});

self.addEventListener("fetch", (event) => {
  if (event.request.mode !== "navigate") return;
  event.respondWith(
    fetch(event.request).catch(() => caches.match(OFFLINE_URL))
  );
});

Put a plain, self-contained page at public/offline.html, with its styles inline so it doesn't depend on anything else being cached. When you change what the worker caches, bump the cache name (offline-v2) so the activate step clears the old one.

Register it

Rails doesn't register the worker for you. Add this to app/javascript/application.js:

if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker.register("/service-worker.js", { scope: "/" });
  });
}

Older Rails apps: a small controller

Before 7.2, the approach from our original post still works: a controller action that renders a JavaScript view at the root path.

# app/controllers/service_worker_controller.rb
class ServiceWorkerController < ApplicationController
  skip_forgery_protection

  def show
    render template: "service_worker/show", layout: false,
           content_type: "application/javascript"
  end
end

# config/routes.rb
get "/service-worker.js" => "service_worker#show"

Put the worker code above in app/views/service_worker/show.js.erb. Because it's ERB, you can list fingerprinted assets to precache with helpers such as asset_path. The original version of this post was missing a fetch handler, so it cached the offline page but never served it. The example above fixes that.

Updates and development

  • How updates arrive. The browser checks for a new version of the script when pages load. If the file has changed by even a byte, the new worker installs, then waits until every tab using the old one is closed. Call self.skipWaiting() in the install handler if you want it to take over sooner.
  • Turn it off while you work. In Chrome DevTools, open Application → Service workers and tick Update on reload or Bypass for network, or click Unregister. Otherwise you'll be debugging yesterday's cached code.
  • Test offline properly. Use DevTools' offline mode, then reload. If you use Turbo, test a link click as well as a full reload, because Turbo loads pages with fetch requests rather than browser navigations.
Worth knowingA service worker that caches too much is harder to fix than one that caches too little, because the bad version keeps serving itself until the browser fetches a new one. Start with an offline page only, and add more caching deliberately.

Common questions

Do I need a gem for this?

Not on Rails 7.2 or later. The built-in PWA controller serves the files. Older apps can use a small controller like the one above.

Is a service worker the same as a background job?

No. Service workers run in the visitor's browser. Background jobs, such as those run by Active Job, run on your server.

Why isn't my service worker registering?

Usually it's served over plain HTTP, from a path that gives it too narrow a scope, or with the wrong content type. Check the Console and the Application panel in DevTools for the error.

Building a Shopify app or a store integration in Rails and want a second opinion on performance? Read why a Shopify app can slow down a store, or see when to use an app, custom code, or something in between. Mozilla's guide to using service workers goes deeper on caching strategies.