DEV Community

Vladimir Elchinov for Session Replay

Posted on

In a Background Tab, setInterval(fn, 1000) Runs Once a Minute

Somebody leaves your dashboard open in a background tab, comes back after lunch, and the number on screen is twenty minutes stale. Or the countdown is wrong. Or they were never logged out when they should have been.

Nothing threw. Nothing appeared in the console. Your timer simply stopped running at the rate you asked for, an hour ago, and never told anybody.

The rule, in Chrome's own words

Chrome applies what it calls intensive throttling when all four of these are true:

  • "The page has been hidden for more than 5 minutes."
  • "The chain count is 5 or greater."
  • "The page has been silent for at least 30 seconds."
  • "WebRTC is not in use."

And then the consequence, stated plainly: "the browser will check timers in this group once per minute."

Once per minute. Not "a bit later than you asked". Not jitter. Your one-second interval becomes a sixty-second one, and the only thing that changed is that the user looked at a different tab.

Your code is "chained" whether you meant it or not

The chain count is the condition people assume will save them, and it will not. Chrome defines it like this:

If you call setTimeout in the same task as a setTimeout callback, the second invocation is "chained". With setInterval, each iteration is part of the chain.

Read that second sentence carefully. Every iteration of a setInterval adds to the chain, so a plain setInterval crosses the threshold of five on its fifth tick and stays there forever.

And the other common shape is chained too: the poll that schedules the next poll at the end of its own callback. That is setTimeout called in the same task as a setTimeout callback, which is the definition.

Between those two, most timer code anybody writes is chained by construction.

The bug this actually causes

The slowdown is the obvious half. The subtle half is that code which counts ticks silently produces wrong answers, rather than slow ones:

// Wrong in a background tab: it is counting ticks, not time.
let elapsed = 0
setInterval(() => {
  elapsed += 1000
  if (elapsed > SESSION_LIMIT) expireSession()
}, 1000)
Enter fullscreen mode Exit fullscreen mode

After five minutes hidden, that runs once a minute, so elapsed advances by one second per minute of real time. A thirty-minute session limit now expires after thirty hours of wall clock. The user is not logged out, and your security control quietly stopped being one.

// Right: it reads the clock, so a late tick is still correct.
const start = Date.now()
setInterval(() => {
  if (Date.now() - start > SESSION_LIMIT) expireSession()
}, 1000)
Enter fullscreen mode Exit fullscreen mode

Both versions run once a minute when hidden. Only the second is still right when it runs. That is the whole fix, and it is the general rule for anything timer-driven: never accumulate, always compare against the clock.

Why you will never see it yourself

Your tab is focused. A visible page gets minimal throttling, which is to say none at all, so the entire behaviour is invisible from the seat you develop in.

Two of the four conditions make it worse, because they create people who genuinely cannot reproduce it. A page that "has made noises in the past 30 seconds" is exempt, and so is one using WebRTC. So a video-call app is fine while a dashboard is not, and the colleague who happens to have a sound playing in that tab will tell you it works for them.

What reaches you instead

The report says the timer was wrong, or the page was stale, or it did not log them out. All three are true, none of them is reproducible on your machine, and the fact that would explain it, that the tab sat in the background for more than five minutes, is not something anybody thinks to mention. It is not a detail about your software. It is a detail about what else they had open.

Top comments (0)