How setInterval Works in JavaScript: Architecture

Understand how JavaScript setInterval works internally. Learn interval scheduling, drift problems, self-correcting timers, hidden tab throttling, and when to choose setTimeout recursion over setInterval.

JavaScriptintermediate
11 min read

setInterval schedules a callback function to fire repeatedly at a fixed interval. Unlike setTimeout which fires once, setInterval keeps firing until you explicitly cancel it. This guide covers how intervals are scheduled, why they drift, the precise difference between setInterval and recursive setTimeout, and patterns for building accurate repeating timers.

Basic Syntax

javascriptjavascript
// setInterval(callback, intervalMs, ...args)
const intervalId = setInterval(callback, interval, arg1, arg2);
clearInterval(intervalId); // Stop it
ParameterTypeDescription
callbackFunctionCalled repeatedly at each interval
intervalNumberMilliseconds between each call
...argsAnyExtra arguments forwarded to callback
ReturnsNumberInterval ID used with clearInterval
javascriptjavascript
let count = 0;
 
const id = setInterval(() => {
  count++;
  console.log(`Tick ${count}`);
 
  if (count === 5) {
    clearInterval(id); // Stop after 5 ticks
    console.log("Stopped");
  }
}, 1000);
 
// Tick 1 (after ~1s)
// Tick 2 (after ~2s)
// ...
// Tick 5 (after ~5s)
// Stopped

How setInterval Schedules Callbacks

Like setTimeout, setInterval is managed by the browser's Web API, not by JavaScript's single-threaded engine. The interval fires in the task queue (macrotask queue):

CodeCode
setInterval(fn, 1000) registered at T=0

T=1000ms: fn queued in task queue
   -> event loop checks: call stack empty? yes -> run fn
   -> fn runs (takes 0ms)

T=2000ms: fn queued again
T=3000ms: fn queued again
...

The key point: the interval clock ticks independently of how long the callback takes.

The Drift Problem

If the callback takes longer than the interval, the queue fills up faster than it can drain:

javascriptjavascript
// Interval: 1000ms, but callback takes 1500ms
let lastRun = Date.now();
 
setInterval(() => {
  const now = Date.now();
  console.log(`Gap since last run: ${now - lastRun}ms`);
  lastRun = now;
 
  // Simulate 1500ms of work
  const start = Date.now();
  while (Date.now() - start < 1500) {}
}, 1000);
 
// Actual output (approximate):
// Gap since last run: 1000ms
// Gap since last run: 1500ms  <-- first callback ran long
// Gap since last run: 1500ms  <-- interval couldn't fire on time

How Browsers Handle Queued Intervals

Different browsers have different policies:

ScenarioBrowser Behavior
Callback finishes before next intervalFires at the next interval normally
Callback overruns the intervalSome browsers skip the overrun tick
Multiple ticks queue upAt most one extra tick is queued (browsers coalesce)

The spec-compliant behavior: if the timer fires and a previous execution of the same interval is still running (waiting to start), the new callback is put in the queue but only ONE pending callback is allowed to queue at a time.

setInterval vs Recursive setTimeout

This is a fundamental difference to understand:

javascriptjavascript
// setInterval schedules the NEXT tick at a FIXED time from the ORIGINAL start
setInterval(() => {
  doWork(); // If this takes 200ms, the NEXT call starts shorter than 1000ms later
}, 1000);
 
// Recursive setTimeout schedules the NEXT call after THIS call finishes
function scheduleRecursive() {
  doWork(); // If this takes 200ms...
  setTimeout(scheduleRecursive, 1000); // NEXT run starts 1000ms from NOW
}
setTimeout(scheduleRecursive, 1000);

Timing Diagrams

CodeCode
setInterval(fn, 1000) where fn takes 200ms:

T=0:    [fn runs 200ms]
T=1000: [fn runs 200ms]
T=2000: [fn runs 200ms]
         |<--- 800ms gap --->|<--- 800ms gap --->|
         (interval was 1000ms but fn itself took 200ms)

Recursive setTimeout(fn, 1000) where fn takes 200ms:

T=0:    [fn runs 200ms] ... 1000ms wait ...
T=1200: [fn runs 200ms] ... 1000ms wait ...
T=2400: [fn runs 200ms] ... 1000ms wait ...
         |<--- 1000ms gap --->|<--- 1000ms gap --->|
         (gap between END of one run and START of next is always 1000ms)

Which to Use

ScenarioBest Choice
Fixed visual updates (clock display)setInterval
Polling where each call can take variable timeRecursive setTimeout
You need a guaranteed gap between executionsRecursive setTimeout
Simple countdown or progresssetInterval

Building a Self-Correcting Timer

setInterval drifts over time because each tick has small time errors that accumulate. A self-correcting timer compensates:

javascriptjavascript
function createAccurateInterval(callback, interval) {
  const start = performance.now();
  let expectedTick = start + interval;
  let timerId = null;
 
  function tick() {
    callback();
 
    const now = performance.now();
    const drift = now - expectedTick;
    expectedTick += interval;
 
    // Adjust next delay to compensate for drift
    const nextDelay = Math.max(0, interval - drift);
    timerId = setTimeout(tick, nextDelay);
  }
 
  timerId = setTimeout(tick, interval);
 
  return {
    stop() {
      clearTimeout(timerId);
    }
  };
}
 
const clock = createAccurateInterval(() => {
  console.log(new Date().toLocaleTimeString());
}, 1000);
 
// Stays accurate even after several minutes

The Accumulating Clock

A common real-world example: a stopwatch timer that stays accurate:

javascriptjavascript
function createStopwatch() {
  let running = false;
  let elapsed = 0;
  let startTime = null;
  let intervalId = null;
 
  function format(ms) {
    const minutes = Math.floor(ms / 60000);
    const seconds = Math.floor((ms % 60000) / 1000);
    const centiseconds = Math.floor((ms % 1000) / 10);
    return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
  }
 
  return {
    start() {
      if (running) return;
      running = true;
      startTime = Date.now() - elapsed;
 
      intervalId = setInterval(() => {
        elapsed = Date.now() - startTime;
        // Use actual Date.now() difference, not accumulated interval ticks
        console.log(format(elapsed));
      }, 10); // Update every 10ms (centisecond precision)
    },
 
    stop() {
      if (!running) return;
      clearInterval(intervalId);
      running = false;
    },
 
    reset() {
      clearInterval(intervalId);
      running = false;
      elapsed = 0;
      startTime = null;
    },
 
    getTime() {
      return elapsed;
    }
  };
}
 
const sw = createStopwatch();
sw.start();
setTimeout(() => { sw.stop(); console.log("Final:", sw.getTime()); }, 3000);

Avoiding Common Mistakes

Mistake 1: Forgetting to Save the ID

javascriptjavascript
// LEAKY: No way to stop this interval
setInterval(() => {
  console.log("Running forever...");
}, 1000);
 
// FIX: Always store the ID
const id = setInterval(() => {
  console.log("Stoppable");
}, 1000);
 
// Later:
clearInterval(id);

Mistake 2: Using setInterval for Animations

javascriptjavascript
// WRONG: setInterval for animation
let x = 0;
setInterval(() => {
  element.style.transform = `translateX(${x++}px)`;
}, 16); // ~60fps but imprecise and not synced to display
 
// RIGHT: requestAnimationFrame for animation
function animate() {
  element.style.transform = `translateX(${x++}px)`;
  if (x < 300) {
    requestAnimationFrame(animate);
  }
}
requestAnimationFrame(animate);

Mistake 3: Interval Leaks in Components

javascriptjavascript
// LEAKY React-like pattern
function setupWidget() {
  const id = setInterval(updateUI, 1000);
  // If the component is destroyed without clearInterval, the interval leaks
}
 
// FIX: Always return cleanup function
function setupWidget() {
  const id = setInterval(updateUI, 1000);
  return () => clearInterval(id); // Cleanup function
}
 
const cleanup = setupWidget();
// When widget is destroyed:
cleanup();

Interval Throttling in Background Tabs

Like setTimeout, setInterval callbacks are throttled in hidden browser tabs (typically to 1-second minimum intervals):

javascriptjavascript
document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    console.log("Tab hidden: intervals will throttle to ~1s minimum");
  } else {
    console.log("Tab visible: intervals run at normal speed");
  }
});
 
// For critical intervals (e.g., WebSocket keepalive), use Web Workers
// which are NOT throttled by tab visibility

Polling With setInterval

setInterval is the natural choice for polling:

javascriptjavascript
function createPoller(fetchFn, intervalMs = 5000) {
  let id = null;
  let active = false;
 
  return {
    start() {
      if (active) return;
      active = true;
      fetchFn(); // Run immediately on start
      id = setInterval(fetchFn, intervalMs);
    },
 
    stop() {
      clearInterval(id);
      id = null;
      active = false;
    },
 
    isActive() {
      return active;
    }
  };
}
 
const statusPoller = createPoller(async () => {
  const res = await fetch("/api/status");
  const data = await res.json();
  console.log("Status:", data.status);
}, 10000);
 
statusPoller.start();
// Later:
statusPoller.stop();
Rune AI

Rune AI

Key Insights

  • Independent clock: The interval timer runs independently of callback duration, so slow callbacks cause gaps shorter than the specified interval
  • Fixed-time vs fixed-gap: setInterval targets a fixed rate from start, recursive setTimeout guarantees a fixed gap after each execution
  • Self-correct with performance.now(): Measure actual elapsed time and adjust the next delay to compensate for accumulated drift
  • Always clear on cleanup: Store every interval ID and call clearInterval when the component or feature is destroyed to avoid memory leaks
  • Background throttling: Intervals in hidden tabs fire at most once per second; use Web Workers for time-critical work that must run in the background
RunePowered by Rune AI

Frequently Asked Questions

Does setInterval guarantee exactly N ms between calls?

No. The interval is a minimum, not a guarantee. Like [`setTimeout`](/tutorials/programming-languages/javascript/javascript-settimeout-behavior-complete-guide), callbacks go through the task queue and can only run when the [call stack](/tutorials/programming-languages/javascript/understanding-the-javascript-call-stack-guide) is empty. Long-running tasks can push interval callbacks later than expected. Use a self-correcting timer based on `performance.now()` for precision.

What happens if I call setInterval inside setInterval?

Each `setInterval` call creates a completely independent timer. Calling `setInterval` inside an interval callback is unusual and typically a design mistake. It creates a new interval for every tick of the outer one, quickly creating an exponentially growing number of timers. Use recursive `setTimeout` or manage the nested interval ID carefully.

How many setInterval timers can I have running simultaneously?

There is no hard specification limit. In practice, browsers handle hundreds of simultaneous timers without issues, though each has overhead. The bigger concern is clarity and memory. Use a single interval for related tasks rather than many separate timers, and always clear intervals when they are no longer needed to avoid [memory leaks](/tutorials/programming-languages/javascript/how-to-prevent-memory-leaks-in-javascript-closures).

Why does clearInterval(id) not work after a page navigation?

When a page navigates away, all timers are automatically cleared by the browser. This is expected behavior. `clearInterval` is only needed to stop a running interval before a navigation, component unmount, or other cleanup scenario within the same page lifecycle.

Is setInterval part of JavaScript or the browser?

`setInterval` is a **Web API** provided by the browser (or the `timers` module in Node.js). It is not part of the ECMAScript language specification. JavaScript itself has no concept of time. The browser hands off timer management to the Web API platform, which then re-enters JavaScript via the task queue when the timer fires.

Conclusion

FeaturesetInterval(fn, N)Recursive setTimeout(fn, N)
Timing referenceFixed from original callFixed from END of last execution
Drift behaviorAccumulates small errorsMaintains gap after execution
Can skip ticksYes (if callback runs long)No (next call is always after gap)
Cancel methodclearInterval(id)clearTimeout(id)
Use forClocks, polling, animationAPI polling, retry loops
setInterval fires a callback repeatedly at a fixed interval by scheduling it as a macrotask through the browser Web API. The interval clock is independent of callback execution duration, which is why slow callbacks cause drift. For accurate repeating work, measure actual elapsed time with performance.now() and self-correct. Always store interval IDs and call clearInterval to prevent memory leaks.