Single-Threaded Architecture
How Node.js runs JavaScript on one thread and why that is a feature, not a limitation
Overview
Key Points
- β’JavaScript executes on exactly one thread in Node.js β only one callback runs at a time
- β’Blocking I/O (files, network, DNS) is offloaded to libuv's thread pool or the OS kernel, not the JS thread
- β’No race conditions on shared JS state because concurrent code literally cannot run at the same time
- β’The single thread becomes a bottleneck only for CPU-intensive work, not for I/O-heavy workloads
- β’This model enables handling tens of thousands of concurrent connections with very low memory overhead
- β’Worker Threads and Child Processes exist specifically to move CPU work off the main thread
Examples
Demonstrating that JS is single-threaded β setTimeout does not interrupt running code
// Even though we schedule a timeout for 0ms,
// it cannot fire while synchronous code is running.
setTimeout(() => {
console.log('timeout fired');
}, 0);
// This loop runs fully before the timeout callback can execute
for (let i = 0; i < 1_000_000; i++) { /* busy work */ }
console.log('sync done');
// Output order:
// sync done
// timeout firedThe event loop only picks up pending callbacks after the current synchronous execution finishes.
I/O does not block the thread β Node continues executing while waiting for the file
const fs = require('fs');
fs.readFile('data.json', 'utf8', (err, data) => {
// This runs later, after the OS reads the file
console.log('file read complete, length:', data.length);
});
// This runs immediately β we did not wait for the file
console.log('registered read callback, moving on...');
// Output:
// registered read callback, moving on...
// file read complete, length: 1234Blocking the event loop β what NOT to do with synchronous CPU work
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/slow') {
// BAD: this blocks all other requests for ~3 seconds
const end = Date.now() + 3000;
while (Date.now() < end) { /* spin */ }
res.end('slow done');
} else {
res.end('fast');
}
});
server.listen(3000);
// While /slow is processing, ANY /fast request also
// has to wait β the entire server is frozen.Move CPU-heavy work to Worker Threads or a child process so the event loop stays free.
Common Mistakes
- Assuming "single-threaded" means Node is slow β it actually outperforms thread-per-request servers for I/O workloads because it avoids context-switching overhead
- Running synchronous CPU loops (sorting huge arrays, crypto without worker) on the main thread and wondering why the server becomes unresponsive
- Thinking that async/await makes code truly parallel β it still runs on the one JS thread, just non-blocking
- Forgetting that libuv's default thread pool size is 4; DNS lookups, fs operations, and some crypto can queue behind each other
Note
Run `node -e "console.log(process.versions)"` to see the exact V8 and libuv versions bundled with your Node install. Understanding which libuv version you have tells you the default UV_THREADPOOL_SIZE and what async operations it supports.
Exercises
Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.
Completed: 0 / 6 exercises
Knowledge Check
Question 1 of 4