Skip to content
JavaScript js async 5 min read

setTimeout & setInterval

Timers are the oldest asynchronous primitive in JavaScript: they let you defer a function until later or run it repeatedly on a schedule. setTimeout runs a callback once after a delay, while setInterval runs it again and again at a fixed cadence. Both are available in the browser and in Node.js, both hand back a handle you can use to cancel the work, and both are deeply tied to how the event loop processes tasks. Understanding them — and their surprising timing quirks — is essential for anything from animations to polling to debouncing user input.

Running code once with setTimeout

setTimeout(callback, delay, ...args) schedules callback to run once after at least delay milliseconds. It returns a timer ID that you can pass to clearTimeout to cancel the pending call before it fires. Any extra arguments after the delay are forwarded to the callback.

const id = setTimeout((name) => {
  console.log(`Hello, ${name}!`);
}, 1000, "Ada");

console.log("scheduled");
// Change your mind within 1s and nothing logs:
// clearTimeout(id);

Output:

scheduled
Hello, Ada!

The delay is a minimum, not a guarantee. The callback is placed on the task queue once the timer elapses, but it only runs when the call stack is empty and the event loop reaches it. A busy main thread will push the callback later than you asked.

Repeating code with setInterval

setInterval(callback, delay) works the same way but re-schedules the callback every delay milliseconds until you stop it with clearInterval. Always capture the returned ID — an interval that nobody clears runs forever and is a classic memory and CPU leak.

let count = 0;
const ticker = setInterval(() => {
  count += 1;
  console.log(`tick ${count}`);
  if (count === 3) clearInterval(ticker);
}, 500);

Output:

tick 1
tick 2
tick 3

Always store interval IDs and clear them when the component, page, or process no longer needs them. In single-page apps, an uncleared interval keeps firing after the view is gone.

The minimum-delay and clamping reality

You can request setTimeout(fn, 4), but the spec clamps nested timers to a minimum of 4ms once you are five or more levels deep in nested timeouts. Browsers also throttle timers in background (inactive) tabs to roughly once per second to save battery, and may further delay timers in tabs that have been hidden for a while. Node.js does not impose the 4ms clamp the same way but is still subject to event-loop scheduling.

BehaviorBrowserNode.js
Minimum nested delay~4ms (after 5 nestings)no fixed clamp
Background-tab throttlingyes (≥1000ms)n/a
Returnsnumeric IDTimeout object
CancelclearTimeout / clearIntervalsame

Because Node returns an object rather than a number, you can call .unref() on it to let the process exit even if the timer is still pending — handy for non-blocking background tasks.

0ms timeouts and the event loop

setTimeout(fn, 0) does not run fn immediately. It schedules fn as a macrotask to run after the current synchronous code finishes and after all pending microtasks (such as resolved promises). It is the simplest way to yield to the browser so it can paint or handle input before continuing.

console.log("1");
setTimeout(() => console.log("2 — macrotask"), 0);
Promise.resolve().then(() => console.log("3 — microtask"));
console.log("4");

Output:

1
4
3 — microtask
2 — macrotask

Note that the promise callback (a microtask) runs before the 0ms timeout (a macrotask), even though the timeout was scheduled first.

Recursive setTimeout vs setInterval

setInterval fires every delay ms regardless of how long the callback takes — if the callback runs longer than the interval, calls can stack up with no gap between them. Recursive setTimeout schedules the next run only after the current one finishes, guaranteeing a fixed delay between executions and giving you the chance to vary the delay or stop cleanly.

let running = true;

function poll() {
  if (!running) return;
  console.log("polling server...");
  // Schedule the next poll only after this one's work is queued:
  setTimeout(poll, 2000);
}

poll();
// Later: running = false; stops it after the next tick.

This pattern is the standard choice for polling and for any repeating task whose duration is unpredictable, because two requests can never overlap.

Debouncing with setTimeout

A common real-world use is debouncing: collapse a burst of rapid events (typing, resizing) into a single call after the activity stops. Each new event clears the previous pending timer and starts a fresh one.

function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const onSearch = debounce((q) => console.log(`Searching: ${q}`), 400);
onSearch("a");
onSearch("ab");
onSearch("abc"); // only this one runs, 400ms after the last keystroke

When to reach for requestAnimationFrame instead

For visual updates — animating a canvas, moving DOM elements, smooth scrolling — do not drive frames with setInterval(draw, 16). Use requestAnimationFrame(draw) instead. It syncs to the display’s refresh rate, passes a high-resolution timestamp for smooth motion, and automatically pauses in background tabs, saving battery.

function loop(timestamp) {
  // draw the next frame using `timestamp` for time-based movement
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

Reserve setTimeout/setInterval for logic and scheduling; reserve requestAnimationFrame for rendering. Mixing them up leads to janky animations and wasted CPU.

Best Practices

  • Always capture timer IDs and call clearTimeout/clearInterval when the work is no longer needed, especially on cleanup or unmount.
  • Treat the delay as a minimum, never an exact guarantee — never rely on a timer for precise timing.
  • Prefer recursive setTimeout over setInterval when a callback’s runtime is variable or when overlapping runs would cause bugs.
  • Use setTimeout(fn, 0) to yield to the event loop, but remember microtasks (promises) run first.
  • Pass arguments via setTimeout’s extra parameters or a closure rather than building strings to eval.
  • In Node.js, use .unref() on timers that should not keep the process alive.
  • Use requestAnimationFrame for anything that renders to the screen.
Last updated June 1, 2026
Was this helpful?