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, the cluster module, 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.