Chapter 32: Node.js Timers Module
1. What is the Timers module in Node.js?
The timers module provides functions to schedule execution of code after a delay or at regular intervals.
These are the four main functions you will use almost every day:
| Function | What it does | Returns | Clears with | Repeats? |
|---|---|---|---|---|
| setTimeout() | Run once after delay | Timeout object | clearTimeout() | No |
| setInterval() | Run repeatedly every X ms | Interval object | clearInterval() | Yes |
| setImmediate() | Run as soon as event loop is idle | Immediate object | clearImmediate() | No |
| setTimeout(…, 0) | Special case — similar to setImmediate | Timeout object | clearTimeout() | No |
All of them are available globally — you do not need to import anything.
|
0 1 2 3 4 5 6 7 8 |
setTimeout(() => console.log("Hello after 2s"), 2000); setInterval(() => console.log("Tick"), 1000); setImmediate(() => console.log("As soon as possible")); |
But in modern code, many people prefer explicit import for clarity:
|
0 1 2 3 4 5 6 |
import { setTimeout, setInterval, setImmediate, clearTimeout } from 'node:timers'; |
2. Important mental model: Timers are not precise
setTimeout(…, 1000) does NOT guarantee execution after exactly 1 second.
It means: “Please run this no sooner than 1000 ms from now — but it might be later.”
Reasons for delay:
- Event loop is busy (long synchronous code)
- CPU is overloaded
- Node.js is garbage collecting
- System is sleeping / low-power mode
- Other timers / I/O events are being processed
Rule of thumb 2025–2026:
If you need exact timing → use a real-time OS or dedicated hardware If you need approximate timing → Node.js timers are perfect
3. Deep comparison of the four timer types
Let’s run a small experiment that shows the real order & behavior.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
console.log("Start – synchronous"); setTimeout(() => { console.log("setTimeout 0 ms"); }, 0); setImmediate(() => { console.log("setImmediate"); }); setInterval(() => { console.log("setInterval (every 1000 ms)"); }, 1000); setTimeout(() => { console.log("setTimeout 100 ms"); }, 100); console.log("End – synchronous"); |
Typical output (very common order on most machines):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
Start – synchronous End – synchronous setImmediate setTimeout 0 ms setTimeout 100 ms setInterval (every 1000 ms) setInterval (every 1000 ms) ... |
Why this order?
- Synchronous code runs immediately
- setImmediate callbacks run after poll phase (when event loop is idle)
- setTimeout(…, 0) callbacks run in timers phase — usually after setImmediate when script was run from I/O context
- setInterval runs repeatedly in timers phase
Very important takeaway:
When you run a script directly (not inside I/O callback), setImmediate usually fires before setTimeout(…, 0)
But when you run inside an I/O callback (like setTimeout, fs.readFile, http request), the order often reverses.
Real test:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
setTimeout(() => { console.log("Inside setTimeout"); setImmediate(() => console.log("setImmediate inside timeout")); setTimeout(() => console.log("setTimeout 0 inside timeout"), 0); }, 0); |
Common output:
|
0 1 2 3 4 5 6 7 8 |
Inside setTimeout setTimeout 0 inside timeout setImmediate inside timeout |
Moral: Never rely on exact order between setImmediate and setTimeout(…, 0) — it depends on context.
4. Real-world patterns you’ll see in 2025–2026
Pattern 1 – Debounce / throttle (very common in APIs)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function debounce(fn, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } // Usage – expensive search API const searchApi = debounce(async (query) => { console.log(`Searching for: ${query}`); // await real API call... }, 600); |
Pattern 2 – Rate limiting / cooldown
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
let lastAction = 0; function canPerformAction() { const now = Date.now(); if (now - lastAction < 5000) { return false; } lastAction = now; return true; } // or better – using setTimeout function cooldown(fn, ms) { let ready = true; return function (...args) { if (!ready) return; ready = false; fn(...args); setTimeout(() => { ready = true; }, ms); }; } |
Pattern 3 – Graceful shutdown timer
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let shuttingDown = false; process.on('SIGTERM', () => { if (shuttingDown) return; shuttingDown = true; console.log('SIGTERM received – shutting down gracefully...'); setTimeout(() => { console.log('Grace period ended – forcing exit'); process.exit(1); }, 10000); // 10 seconds grace period // Close DB, stop accepting connections, etc. }); |
Pattern 4 – setInterval with dynamic interval (advanced)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let intervalId; let currentInterval = 1000; function startMonitoring() { intervalId = setInterval(() => { console.log('Checking health...'); // Example: increase interval when stable if (someConditionIsStable()) { clearInterval(intervalId); currentInterval = 5000; startMonitoring(); // restart with longer interval } }, currentInterval); } |
5. Common mistakes & how to avoid them
| Mistake | Consequence | Fix / Best Practice |
|---|---|---|
| Using setTimeout(…, 0) thinking it runs immediately | It does not — runs later in timers phase | Use setImmediate() when you want next tick |
| Not clearing intervals/timeouts | Memory leak, callbacks keep running | Always keep reference & call clearInterval() |
| Blocking event loop inside timer callback | Entire application becomes unresponsive | Move heavy work to Worker Threads |
| Relying on exact timing | Timing is never exact | Use real-time libraries or external schedulers |
| Forgetting to handle errors in callbacks | Silent failures | Wrap callbacks in try/catch or use domain (legacy) |
Summary – Quick cheat sheet 2025–2026
| You want to… | Use this | Typical real-world example |
|---|---|---|
| Run code once after delay | setTimeout(fn, ms) | Debounce input, retry after failure, cooldown |
| Run code repeatedly | setInterval(fn, ms) | Polling, health checks, cron-like tasks |
| Run code as soon as event loop is idle | setImmediate(fn) | After current I/O callback, before next timers |
| Run code in next event loop tick | setTimeout(fn, 0) | Similar to setImmediate (order depends on context) |
| Cancel a timer | clearTimeout(id) / clearInterval(id) | Stop debounce, stop polling, cancel retry |
Would you like to go much deeper into any specific area?
- Debounce vs Throttle – full implementations & real UI/API examples
- setImmediate vs process.nextTick – detailed order explanation
- Timer memory leaks – how to detect & prevent in long-running apps
- Building a real scheduler using timers (cron-like, retry queue)
- Timers in Worker Threads – different behavior
Just tell me which direction you want — I’ll continue with detailed, production-ready examples. 😊
