Skip to content

chrome.alarms in Manifest V3: Periodic Tasks That Survive a Service Worker Restart

6 min readModerok team

Why setInterval fails in an MV3 service worker, how chrome.alarms schedules periodic tasks that wake a terminated worker, and the 30-second minimum.

If you need a Chrome extension to run a periodic task in Manifest V3, setInterval and setTimeout are the wrong tools: they are cleared the moment the service worker is torn down, which happens after about 30 seconds of inactivity. The tool that works is chrome.alarms. An alarm lives outside the worker, fires on a schedule you set, and wakes a terminated service worker to run your handler. The catch: the minimum interval is 30 seconds (as of Chrome 120; it was 1 minute before that), and you should recreate your alarms every time the worker starts rather than assume they survived.

This post covers why timers fail, how to schedule an alarm that keeps firing, the exact minimum interval, and the startup pattern that keeps a single alarm from becoming a dozen.

Why setInterval does not work in a service worker

A Manifest V3 background context is a service worker, not a persistent page. It spins up to handle an event, runs, and shuts down when it goes idle. Any setTimeout or setInterval you scheduled is discarded along with the worker's memory. A 10-minute setInterval in a worker that sleeps after 30 seconds will almost never reach its second tick.

Worse, timers do not count as "work." Chrome ignores pending timers when it decides whether the worker is idle, so a setInterval cannot even keep the worker alive to fire itself. If you have been fighting a background job that runs once and then silently stops, this is why. (For the full picture of when and why the worker stops, see why your service worker keeps stopping.)

chrome.alarms sidesteps all of this because the schedule is held by the browser, not by your worker. When an alarm is due, Chrome starts the service worker if it is not already running and dispatches the onAlarm event.

Request the alarms permission

chrome.alarms requires the "alarms" permission. It is not a host permission and does not trigger a scary install warning; add it to the permissions array:

{
  "manifest_version": 3,
  "name": "My Extension",
  "permissions": ["alarms", "storage"],
  "background": { "service_worker": "background.js" }
}

Without it, chrome.alarms is undefined and every call throws.

Create an alarm and handle it

Two pieces are needed: create the alarm, and register an onAlarm listener. The listener must be registered synchronously at the top level of the service worker, not inside a promise callback. When Chrome wakes the worker to deliver the alarm, it re-runs the top-level script; if your addListener call sits behind an await, the listener may not be attached yet when the event arrives and the alarm is missed.

// background.js (top level of the service worker)

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === "sync") {
    syncData();
  }
});

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create("sync", { periodInMinutes: 30 });
});

chrome.alarms.create() takes an optional name and an alarmInfo object:

chrome.alarms.create(name, {
  when,             // absolute time in ms since epoch (mutually exclusive with delayInMinutes)
  delayInMinutes,   // fire once after this delay
  periodInMinutes,  // repeat every this many minutes
});

Supply periodInMinutes for a repeating alarm, delayInMinutes (or when) for a one-shot. If you give a name that already exists, the existing alarm is overwritten rather than duplicated, which is convenient but easy to misread as "create only if missing" (it is not).

The onAlarm callback receives an Alarm object with name, scheduledTime (ms since epoch), and, for repeating alarms, periodInMinutes. Branch on alarm.name when you have more than one.

The 30-second minimum interval

This is the number people miss: in a published extension, chrome.alarms will not fire more often than every 30 seconds. That floor changed in Chrome 120; before that release the minimum was 1 minute. To ask for the fastest supported cadence, use periodInMinutes: 0.5:

chrome.alarms.create("poll", { periodInMinutes: 0.5 }); // fires every 30 seconds

Values below 0.5 are not honored. If you pass periodInMinutes: 0.25, Chrome logs a warning and fires the alarm every 30 seconds anyway, not every 15. There is no supported way to schedule faster than 30 seconds from a packed, Web Store extension. (An extension loaded unpacked during development is exempt from the throttle, which is exactly why a schedule that works while you are testing can quietly slow down once it is published.)

If your task genuinely needs to run every few seconds, the alarms API is the wrong fit and, honestly, so is a service worker that keeps waking every few seconds. Rework the task to batch its work into a 30-second (or longer) pass, or drive it from the events that already wake your worker.

Do not assume the alarm is still there

An alarm reliably survives the service worker itself being torn down; that is the whole point. In Chrome, alarms are also persistent by default (the persistAcrossSessions option defaults to true), so they generally survive a browser restart and an extension reload too. There are two gaps that bite you anyway. First, alarms are always cleared when the extension updates, so every user who takes an update loses the alarm until something recreates it. Second, that default persistence is a Chrome behavior; other browsers do not guarantee it, so portable code cannot lean on it.

Treat alarm existence as best-effort state, not durable state. The robust pattern is to make sure your alarms exist every time the worker starts, and to check before creating so you do not stack duplicates or reset a running period:

async function ensureSyncAlarm() {
  const existing = await chrome.alarms.get("sync");
  if (!existing) {
    chrome.alarms.create("sync", { periodInMinutes: 30 });
  }
}

// Runs whenever the worker cold-starts, plus the usual lifecycle events.
ensureSyncAlarm();
chrome.runtime.onStartup.addListener(ensureSyncAlarm);
chrome.runtime.onInstalled.addListener(ensureSyncAlarm);

chrome.alarms.get(name) returns the alarm or undefined; chrome.alarms.getAll() returns them all; chrome.alarms.clear(name) and chrome.alarms.clearAll() remove them. All of these return promises in Manifest V3, so you can await them.

What alarms are good for

Alarms fit any recurring, low-frequency background job in an extension: refreshing a cached token or feed every 30 minutes, retrying a failed upload, running a daily cleanup, or flushing data you have been buffering in chrome.storage. Because they wake the worker, they pair naturally with storage: the alarm fires, the worker reads what it needs from chrome.storage.local, does the work, and writes the result back. Just remember that storage has its own limits when you buffer between runs (see chrome.storage.local quota limits).

What alarms are not good for: precise timing, sub-minute cadence, or anything that must run at an exact wall-clock instant. Chrome may fire an alarm a little late if the machine was asleep or busy, and it batches nearby alarms to save power. Design the handler to be idempotent and to tolerate being called late.

Where analytics fits

A common reason people reach for a periodic timer is to flush queued analytics or telemetry on an interval. If you are using Moderok, you do not have to wire up an alarm for that. The Moderok SDK debounces sends in the service worker and persists retryable failures to chrome.storage.local, then retries them with backoff on later runs. That design does not depend on a timer staying resident, which is exactly the failure mode this post is about. If you are running your own periodic sync on top of that, chrome.alarms is the right scheduler and the startup-recreate pattern above is the right way to use it.

Building an extension and want to see installs, active users, and event trends without running your own cron jobs? Start with the Moderok docs.