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
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.