Explain Node's single-threaded, non-blocking I/O model. How does it handle concurrency?
3 minintermediatenodejsevent-loopconcurrencynon-blockingsingle-threaded
Quick Answer
Your JavaScript runs on a single main thread with an event loop, so you never manage locks around your code. Concurrency comes from offloading I/O to libuv (its event loop and thread pool) so the main thread stays free while operations complete in the background, then their callbacks are queued.
Detailed Answer
Answer: Node runs your JavaScript on one thread, but achieves high concurrency by never waiting on that thread.
Single-threaded (for your code):
- There is one call stack executing your JavaScript. You never deal with data races or locks in your own logic because only one piece of JS runs at a time.
Non-blocking I/O:
- Instead of blocking the thread while reading a file or querying a database, Node hands the operation to libuv, which uses the OS's async facilities (epoll, kqueue, IOCP) or its thread pool.
- The main thread immediately continues to the next line. When the operation completes, its callback is placed on a queue and the event loop runs it when the stack is idle.
// Blocking (bad on the main thread) — nothing else runs during the read
const data = fs.readFileSync('big.log');
// Non-blocking — the thread is free to do other work meanwhile
fs.readFile('big.log', (err, data) => { /* ... */ });
So where does concurrency come from?
- I/O concurrency: many sockets/files can be "in flight" at once because the OS/libuv handle them; Node just reacts to completions.
- CPU work still blocks the single thread. For CPU-bound tasks you must use
worker_threads, theclustermodule, or child processes.
The trade-off: Node excels at I/O-bound workloads (APIs, proxies, real-time apps) with low memory overhead per connection, but a single long synchronous computation (e.g., a tight loop or heavy JSON.parse) blocks everything.