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.
Table of Contents
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.
Alarms vs. Other Scheduling Approaches#
When should you use Chrome Alarms versus alternatives? The answer depends on your minimum interval and reliability requirements.
| Feature | Approach | Min Interval | Survives SW Restart | Precision | Best For |
|---|---|---|---|---|---|
| chrome.alarms | ~1 minute | Yes | ±30 seconds | Most background tasks | |
| setTimeout | 1 ms | No | High | Short delays within active SW | |
| setInterval | 1 ms | No | High | Never use in MV3 SW | |
| chrome.offscreen + Worker | 1 ms | Partial | High | Sub-minute tasks (workaround) | |
| External server push | Real-time | N/A | High | Real-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 });
}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()?
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
Background Service Workers: A Deep Dive
Master Chrome extension service workers: lifecycle events, state persistence, alarm scheduling, message passing, and debugging techniques for Manifest V3.
Debugging Chrome Extensions Like a Pro
Master Chrome extension debugging with DevTools. Debug service workers, content scripts, popups, storage, and network requests with practical techniques and code examples.
The Complete Guide to Manifest V3 in 2026
Everything you need to know about Manifest V3 in 2026: service workers, declarativeNetRequest, storage changes, and migration strategies for Chrome extension developers.