Development12 min read

Chrome Alarms API and Scheduling Background Tasks

Master the Chrome Alarms API for scheduling background tasks. Covers periodic alarms, one-shot timers, service worker wake-up, sub-minute alternatives, and real-world patterns.

C
CWS Kit Team
Share

Chrome Alarms API & Background Scheduling

Reliable task scheduling in the Manifest V3 world

The Chrome Alarms API is how your Manifest V3 extension wakes up and does work when the user is not actively interacting with it. Sync data every 30 minutes. Check for updates once a day. Flush analytics events every 5 minutes. Rotate an authentication token before it expires. Without alarms, your service worker sleeps — and sleeping service workers do nothing.

MV3 killed persistent background pages. Your service worker terminates after roughly 30 seconds of inactivity (sometimes sooner, sometimes later — Chrome makes no guarantees). setTimeout and setInterval die with the worker. The Alarms API is the only Chrome-sanctioned way to schedule work that survives service worker termination. Understanding its capabilities, limitations, and edge cases is not optional for MV3 development — it is foundational.

API Reference: The Essentials#

The API surface is small: four methods and one event. But the behavior under the hood has nuances that catch developers off guard.

// Create an alarm
chrome.alarms.create("sync-data", {
  delayInMinutes: 1,       // First fire: 1 minute from now
  periodInMinutes: 30,     // Then every 30 minutes
});
 
// Create a one-shot alarm (no periodInMinutes)
chrome.alarms.create("token-refresh", {
  when: Date.now() + 3600000, // Absolute timestamp: 1 hour from now
});
 
// Get alarm info
const alarm = await chrome.alarms.get("sync-data");
console.log(alarm?.scheduledTime); // Next fire time (epoch ms)
 
// List all alarms
const allAlarms = await chrome.alarms.getAll();
 
// Clear a specific alarm
await chrome.alarms.clear("sync-data");
 
// Clear all alarms
await chrome.alarms.clearAll();
 
// Listen for alarm fires
chrome.alarms.onAlarm.addListener((alarm) => {
  switch (alarm.name) {
    case "sync-data":
      handleDataSync();
      break;
    case "token-refresh":
      handleTokenRefresh();
      break;
  }
});

The alarms permission is required in your manifest:

{
  "permissions": ["alarms"]
}

Scheduling Patterns#

Different tasks require different scheduling strategies. Here are the four patterns you will use most.

**Use case:** Sync data, check for updates, flush event buffers. Create a repeating alarm with `periodInMinutes`. The minimum period is 1 minute (Chrome silently rounds up anything lower to 1 minute, though older docs mentioned 0.5 minutes in development). For tasks that run every 30 minutes or more, this is straightforward and reliable. ```typescript // Set up periodic sync on extension install chrome.runtime.onInstalled.addListener(() => { chrome.alarms.create('periodic-sync', { delayInMinutes: 1, periodInMinutes: 30, }); }); chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name === 'periodic-sync') { const { lastSync } = await chrome.storage.local.get('lastSync'); const data = await fetchUpdates(lastSync); await chrome.storage.local.set({ ...data, lastSync: Date.now(), }); } }); ``` Always re-create alarms in `onInstalled` — alarms persist across service worker restarts but are cleared on extension update.

Alarms vs. Other Scheduling Approaches#

When should you use Chrome Alarms versus alternatives? The answer depends on your minimum interval and reliability requirements.

FeatureApproachMin IntervalSurvives SW RestartPrecisionBest For
chrome.alarms~1 minuteYes±30 secondsMost background tasks
setTimeout1 msNoHighShort delays within active SW
setInterval1 msNoHighNever use in MV3 SW
chrome.offscreen + Worker1 msPartialHighSub-minute tasks (workaround)
External server pushReal-timeN/AHighReal-time notifications

setTimeout has exactly one valid use in a service worker: scheduling work that must happen within the current execution context. If you need to debounce a storage write by 500ms, setTimeout is fine. But never rely on it for work that must happen in the future — the service worker may terminate before it fires.

setInterval should never appear in a MV3 service worker. It creates the illusion of periodic execution, but the service worker terminates and takes the interval with it.

Edge Cases and Gotchas#

The Alarms API has behaviors that surprise even experienced extension developers. These are the ones that cause bugs in production.

Sub-Minute Scheduling#

Sometimes one minute is not fast enough. Real-time UI updates, rapid polling during active use, or animation-like background effects need sub-second or sub-minute timing. The Alarms API cannot help you here, but there are workarounds.

Offscreen documents. MV3 introduced chrome.offscreen.createDocument(), which creates an invisible document that runs in a page context (not a service worker). This document can use setInterval reliably because it is not subject to service worker lifecycle. Use it for sub-minute polling during active sessions, then tear it down when not needed.

// Create an offscreen document for rapid polling
async function startRapidPolling(): Promise<void> {
  const contexts = await chrome.runtime.getContexts({
    contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT],
  });
 
  if (contexts.length === 0) {
    await chrome.offscreen.createDocument({
      url: "offscreen.html",
      reasons: [chrome.offscreen.Reason.BLOBS],
      justification: "Rapid polling for real-time data sync",
    });
  }
 
  // Send message to offscreen document to start polling
  chrome.runtime.sendMessage({ action: "start-rapid-poll", interval: 5000 });
}
// offscreen.ts — runs in the offscreen document context
let pollInterval: number | undefined;
 
chrome.runtime.onMessage.addListener((msg) => {
  if (msg.action === "start-rapid-poll") {
    if (pollInterval) clearInterval(pollInterval);
    pollInterval = setInterval(async () => {
      const data = await fetchLatestData();
      // Send data back to service worker
      chrome.runtime.sendMessage({ action: "poll-result", data });
    }, msg.interval);
  }
 
  if (msg.action === "stop-rapid-poll") {
    if (pollInterval) clearInterval(pollInterval);
    pollInterval = undefined;
  }
});

Keep-alive patterns. Some developers use Chrome ports or periodic chrome.runtime.sendMessage calls from a popup or content script to keep the service worker alive. This works but is fragile and wastes resources. Only use it when the user is actively interacting with your extension and sub-second responsiveness is genuinely needed.

Debugging Alarms#

When alarms do not fire as expected, debugging is frustrating because the service worker may not have a DevTools session attached when the alarm fires. Here are practical debugging techniques.

// Debug logging wrapper for alarm handler
chrome.alarms.onAlarm.addListener(async (alarm) => {
  const now = Date.now();
  const drift = now - alarm.scheduledTime;
 
  // Log to storage since console may not be visible
  const { alarmLog = [] } = await chrome.storage.local.get("alarmLog");
  alarmLog.push({
    name: alarm.name,
    scheduledTime: alarm.scheduledTime,
    actualTime: now,
    driftMs: drift,
    timestamp: new Date().toISOString(),
  });
 
  // Keep only last 50 entries
  if (alarmLog.length > 50) alarmLog.splice(0, alarmLog.length - 50);
  await chrome.storage.local.set({ alarmLog });
 
  // Dispatch to handler
  await handleAlarm(alarm);
});

You can inspect this log anytime from the extension's popup, options page, or DevTools console:

// View alarm log from DevTools console
chrome.storage.local.get("alarmLog", (result) => {
  console.table(result.alarmLog);
});

Also check chrome://extensions — click "service worker" to attach DevTools. The "Alarms" section in Chrome's internal extension debugging tools shows registered alarms and their next scheduled fire time.

Real-World Pattern: Intelligent Sync#

Here is a complete pattern that combines multiple alarm concepts: periodic sync with backoff on failure, battery awareness, and deduplication.

interface SyncState {
  lastSuccessfulSync: number;
  consecutiveFailures: number;
  isSyncing: boolean;
}
 
const SYNC_ALARM = "intelligent-sync";
const BASE_PERIOD = 15; // minutes
const MAX_PERIOD = 240; // 4 hours
 
chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create(SYNC_ALARM, {
    delayInMinutes: 1,
    periodInMinutes: BASE_PERIOD,
  });
});
 
chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name !== SYNC_ALARM) return;
 
  const state = await getSyncState();
 
  // Prevent overlapping syncs
  if (state.isSyncing) return;
 
  await setSyncState({ ...state, isSyncing: true });
 
  try {
    await performSync();
 
    // Success: reset to base period
    await setSyncState({
      lastSuccessfulSync: Date.now(),
      consecutiveFailures: 0,
      isSyncing: false,
    });
 
    // Re-create with base period if we were in backoff
    if (state.consecutiveFailures > 0) {
      await chrome.alarms.create(SYNC_ALARM, {
        delayInMinutes: BASE_PERIOD,
        periodInMinutes: BASE_PERIOD,
      });
    }
  } catch (error) {
    const failures = state.consecutiveFailures + 1;
    const backoffPeriod = Math.min(
      BASE_PERIOD * Math.pow(2, failures),
      MAX_PERIOD
    );
 
    await setSyncState({
      ...state,
      consecutiveFailures: failures,
      isSyncing: false,
    });
 
    // Re-create alarm with longer period
    await chrome.alarms.create(SYNC_ALARM, {
      delayInMinutes: backoffPeriod,
      periodInMinutes: backoffPeriod,
    });
  }
});
 
async function getSyncState(): Promise<SyncState> {
  const { syncState } = await chrome.storage.session.get("syncState");
  return syncState ?? {
    lastSuccessfulSync: 0,
    consecutiveFailures: 0,
    isSyncing: false,
  };
}
 
async function setSyncState(state: SyncState): Promise<void> {
  await chrome.storage.session.set({ syncState: state });
}
Knowledge Check

1. What happens to chrome.alarms when a Chrome extension is updated?

2. What is the minimum practical period for chrome.alarms.create()?

3. Where should you register chrome.alarms.onAlarm.addListener()?

Alarms API Summary

The Chrome Alarms API is the only reliable way to schedule background work in MV3 extensions. Always re-create alarms in the onInstalled listener. Register onAlarm listeners synchronously at the top level. Use backoff patterns for network-dependent tasks. For sub-minute scheduling, use offscreen documents. Log alarm events to storage for debugging since the service worker console may not be attached when alarms fire.

Continue reading

Related articles

View all posts