Skip to the solution
HardBackend8 min read

What do unref() and ref() do on a Node.js Timeout, and why do timers drift?

388 views

The problem

Question presented to candidate: "You add a setInterval for a periodic background task, and now your CLI tool refuses to exit even after its real work is done. What is happening, and what is the fix?"

What a strong answer should cover:

  • Node's event loop keeps a process alive as long as there is pending work β€” a scheduled timer counts as pending work by default, which is precisely why a lingering setInterval or setTimeout prevents a script from exiting even after everything else has finished.
  • πŸ“Œ Timeout.unref() tells Node not to count this timer when deciding whether to keep the process alive. If nothing else is keeping the event loop alive, the process exits, and this specific timer simply never fires again β€” it does not get force-run or its callback skipped-but-logged, it just stops mattering to the exit decision.
  • πŸ“Œ Timeout.ref() reverses that β€” it puts the timer back into the group of things that do keep the process alive. Calling ref() on an already-ref'd timer, or unref() on an already-unref'd one, is a harmless no-op.
  • The common real use case: a periodic health-check or keep-alive timer that should run while other real work is happening, but should never be the reason a script or CLI tool hangs around after that real work is done.
  • πŸ“Œ Timer drift: setInterval schedules its next firing relative to when the current callback finishes, not against a fixed absolute clock. If a callback takes longer than the interval itself, every subsequent tick is pushed later by that same amount β€” and that offset does not self-correct; it persists into all following ticks.
  • The practical consequence: setInterval is not a stopwatch or a scheduler with hard real-time guarantees. For anything that genuinely needs to run at a fixed wall-clock cadence regardless of how long each execution takes, the standard fix is to compute the next target time explicitly and schedule a fresh setTimeout for exactly that remaining duration, rather than trusting setInterval's fixed nominal period.

Clarifying questions expected:

  • "Is this timer meant to be the ONLY thing keeping the process alive, or does real request-handling already do that?" β€” decides whether unref() is even relevant here.
  • "Does this interval's callback ever do work that could take longer than the interval itself?" β€” that is exactly the condition that produces drift.

Code / implementation expected: Yes β€” showing the actual process-exit timing difference with and without unref(), and a real measured drift after a slow callback, is far more convincing than describing either behavior.

  • timers
  • event-loop
  • unref
  • setinterval

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 9 sections

Read the code

JavaScriptunref()/ref() changing process exit behavior, and measured setInterval drift after one slow callback
// unref(): the process does NOT wait for this timer.
const t = setInterval(() => console.log("should never print"), 5000);
t.unref();
console.log("process exits almost immediately, not after 5s");
// Verified: real 0m0.093s (measured with 'time node ...')

// ref(): reverses it β€” the process DOES wait.
const t2 = setInterval(() => { console.log("tick"); clearInterval(t2); }, 300);
t2.unref();
t2.ref();
// Verified: process waited ~0.4s for the tick to actually fire.

// Drift: setInterval reschedules relative to when the callback FINISHES.
const start = Date.now();
let n = 0;
const id = setInterval(() => {
  n++;
  console.log(`tick ${n}: drift=${(Date.now() - start) - n * 100}ms`);
  if (n === 3) {
    const busyUntil = Date.now() + 250; // simulate slow work
    while (Date.now() < busyUntil) {}
  }
  if (n === 6) clearInterval(id);
}, 100);
// Measured: drift stayed ~1-2ms for ticks 1-3, then jumped to a PERMANENT
// ~150-167ms after the slow tick 3 β€” it never caught back up.

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 142 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

How do you secure your Node.js API endpoints?

Continue