Minimum Alarm Period and Throttling

Why a sub-minute chrome.alarms period works unpacked and is clamped for users, how the first delay differs from the repeat period, and what to use when you genuinely need faster ticks.

Published July 25, 2026 Updated July 25, 2026 8 min read
Table of Contents

You set periodInMinutes: 0.25, watch it tick every fifteen seconds in your unpacked build, ship it, and users get one tick a minute. Nothing errored, nothing warned, and getAll reports the period you asked for on your machine and a different one on theirs. This page is part of Alarms & Scheduling Background Jobs and explains the clamp, the one exception to it, and what to do when a minute is genuinely too slow.

Root cause: the clamp applies to packed extensions only

Browsers clamp repeating alarms to a one-minute minimum for installed extensions, and deliberately do not clamp them for unpacked development builds — the reasoning being that a developer iterating on a timer should not wait a minute per iteration. The result is a difference in behaviour between your machine and every user’s, in the direction least likely to be noticed.

Requested period against delivered periodFor each requested periodInMinutes, the interval actually delivered in an unpacked build and in an installed extension.0.1 requested — unpacked6 shonoured while developing0.1 requested — installed60 sclamped0.5 requested — installed60 sclamped1 requested — installed60 sthe floor5 requested — installed300 shonoured
Everything below one minute collapses to the same delivered interval once the extension is installed.

Step 1. Separate the first delay from the repeat period

delayInMinutes controls when the alarm first fires; periodInMinutes controls the repeat. Only the repeat is clamped, so a short first delay is a legitimate way to get prompt initial work without asking for a sub-minute cadence.

1// background.js — fire in about six seconds, then settle to a legal cadence
2await chrome.alarms.create("import-chunk", {
3  delayInMinutes: 0.1,   // first firing: not subject to the repeat clamp
4  periodInMinutes: 1,    // repeat: the minimum the browser will honour
5});

Execution context: Extension service worker. This is the shape used by the chunked-job pattern: the first chunk starts almost immediately so the user sees progress, and subsequent chunks run at the fastest legal cadence. Asking for periodInMinutes: 0.1 here would change nothing for users and would make the development build behave differently from production, which is worse than the honest one-minute value.

Step 2. Clamp on your own side so both builds agree

The most valuable thing you can do about the clamp is apply it yourself. A development build that behaves like production is worth more than a fast feedback loop on a timer.

 1// scheduling.js
 2export const MIN_PERIOD_MINUTES = 1;
 3
 4export async function schedule(name, requestedMinutes, { firstDelay = 0.1 } = {}) {
 5  const periodInMinutes = Math.max(MIN_PERIOD_MINUTES, Number(requestedMinutes) || MIN_PERIOD_MINUTES);
 6
 7  if (periodInMinutes !== Number(requestedMinutes)) {
 8    console.warn(`alarm "${name}": ${requestedMinutes} min clamped to ${periodInMinutes} min`);
 9  }
10
11  await chrome.alarms.create(name, { delayInMinutes: firstDelay, periodInMinutes });
12  return periodInMinutes;
13}

Execution context: Shared scheduling module imported by the service worker. The warning is the useful part: it turns a silent divergence into a line in the console during development, at the moment the wrong value is requested rather than weeks later in a user report. Returning the effective period lets callers display or log a cadence that matches reality.

What to do when one minute is too slowThree alternatives to a sub-minute alarm: a self-rescheduling chain, an event-driven trigger, and a port that keeps the worker alive for a bounded burst.Need sub-minute workpolling, progress, countdownpick by what is actually driving the cadenceSelf-rescheduling chainone-shot, recreated each runEvent-driven insteadonUpdated, onChanged, onMessageBounded burstopen port, close when done
Each of these is a different trade — the first costs wakes, the second costs nothing, the third costs a worker that cannot idle.

Step 3. Use a self-rescheduling one-shot only when you mean it

A one-shot alarm recreated at the end of each run is not clamped the way a repeat period is — but each firing is still a full worker wake, so a fifteen-second chain means four cold starts a minute for as long as it runs. That is acceptable for a bounded job with a visible end, and unacceptable as a permanent background cadence.

 1// background.js — a bounded fast chain, with a hard stop
 2const FAST_TICK_MINUTES = 0.25;
 3const MAX_FAST_TICKS = 40;              // about ten minutes, then stop
 4
 5chrome.alarms.onAlarm.addListener((alarm) => {
 6  if (alarm.name !== "fast-tick") return;
 7
 8  (async () => {
 9    const { fastTicks = 0 } = await chrome.storage.session.get("fastTicks");
10    const done = await advanceJob();
11
12    if (done || fastTicks + 1 >= MAX_FAST_TICKS) {
13      await chrome.alarms.clear("fast-tick");
14      await chrome.storage.session.remove("fastTicks");
15      return;
16    }
17
18    await chrome.storage.session.set({ fastTicks: fastTicks + 1 });
19    await chrome.alarms.create("fast-tick", { delayInMinutes: FAST_TICK_MINUTES });
20  })();
21});

Execution context: Extension service worker. Counting the ticks in storage.session rather than module scope is what makes the hard stop reliable across evictions — without it, an eviction resets the counter and the chain runs forever. The hard stop is not optional: a fast chain with no termination condition is the pattern that gets an extension flagged for battery use.

Step 4. Prefer an event to a poll

Most sub-minute polling exists because something else was hard to observe. Before building a fast chain, check whether the thing you are polling for has an event: tab updates, storage changes, network rules matching, downloads, idle state and window focus all do.

Common polling loops and the event that replaces themWhat extensions typically poll for, the event that reports the same change, and whether an alarm is still needed as a backstop.Polled forEvent to use insteadAlarm still useful?Did the tab navigate?tabs.onUpdatedNoDid settings change?storage.onChangedNoIs the user idle?idle.onStateChangedNoDid a download finish?downloads.onChangedNoIs the server done?No eventYes — slow pollHas a deadline passed?No eventYes — one-shot at the time
The right-hand column is rarely empty — an event plus a slow alarm beats a fast poll on every axis.

The last two rows are the honest cases for an alarm, and both are well served by a period measured in minutes. A deadline in particular should be a one-shot alarm scheduled with when at the exact time, not a poll that checks whether the moment has arrived — the browser is better at waiting than your handler is.

Step 5. Measure what the cadence actually costs

Before defending a fast cadence, price it. Each firing is a full cold start, so the cost of a schedule is roughly the cold-start cost times the number of firings per hour — and that number is easy to get wrong by an order of magnitude when reasoning about it rather than measuring.

 1// background.js — a temporary instrument, removed before shipping
 2chrome.alarms.onAlarm.addListener((alarm) => {
 3  (async () => {
 4    const key = `stats:${alarm.name}`;
 5    const { [key]: stats = { firings: 0, since: Date.now() } } = await chrome.storage.session.get(key);
 6    const next = { firings: stats.firings + 1, since: stats.since };
 7    await chrome.storage.session.set({ [key]: next });
 8
 9    const hours = (Date.now() - next.since) / 3_600_000;
10    if (hours > 0.05) {
11      console.info(`${alarm.name}: ${(next.firings / hours).toFixed(1)} firings/hour`);
12    }
13  })();
14});

Execution context: Extension service worker, during development only. Counting in storage.session keeps the figure meaningful across evictions within a browsing session and discards it on restart, which is the right window for this measurement. A one-minute alarm is sixty cold starts an hour; multiply by the cold-start figure from your own profile and the answer usually settles the question of whether the cadence is justified — see reducing service worker cold start latency for how to lower the per-firing cost when the cadence genuinely cannot move.

Cross-browser variation

  • Chrome 120+: repeating alarms clamp to one minute for installed extensions; unpacked builds are exempt. delayInMinutes for the first firing is not clamped in the same way.
  • Firefox 121+: applies the same one-minute floor under MV3. Its more forgiving background context can mask the cost of a fast chain, so measure wake frequency in Chrome.
  • Safari 17+: clamps at least as aggressively and adds power-state throttling, so a one-minute alarm may effectively deliver less often on battery.
  • All engines: the clamp is silent. Nothing throws, nothing warns, and getAll may echo back the period you requested — which is why the client-side clamp in step 2 earns its place.

Verification

  1. Set periodInMinutes: 0.25, load the extension unpacked, and log Date.now() per firing. Expect roughly fifteen seconds.
  2. Pack the extension, install it from the packed file, and repeat. Expect roughly sixty seconds — the same code, a different cadence.
  3. Enable the client-side clamp and confirm the console warning names the alarm and both periods.
  4. Run the fast chain and confirm it stops at the tick cap even when you force an eviction partway through.

FAQ

Why is my sub-minute alarm honoured while developing?

Because unpacked extensions are exempt from the clamp by design, so timer iteration is not painful during development. It is also why the difference is so easy to ship.

Is delayInMinutes clamped too?

The first delay is treated separately from the repeat period, which is why the delay-plus-period shape in step 1 is worth using. Do not rely on an extremely small delay for a cadence, though — use it once, for promptness.

Can I use setInterval for faster ticks?

Only for the lifetime of one wake, and it will disappear with the worker. For a bounded burst inside a single handler that is fine; as a background cadence it is the bug that alarms exist to fix.

Does a fast alarm chain hurt battery life?

Yes, measurably, because each firing is a full worker cold start. That is the reason for the hard tick cap — a bounded burst is defensible, a permanent fast chain is not.

Other Core APIs & Cross-Browser Data Management Resources