NodeJSConcurrency in Node.js

Concurrency in Node.js

Node has a reputation for being "single-threaded", which confuses people: how does a single thread serve thousands of simultaneous connections? The answer is that Node achieves concurrency (making progress on many tasks) without parallelism (running many tasks at the exact same instant) — through its asynchronous, non-blocking event loop. When you do need true parallelism for CPU-bound work or to use every core, Node gives you worker threads, child processes, and the cluster module. This page draws the crucial distinctions and maps out which tool fits which job.

Concurrency vs parallelism — not the same thing

Concurrency

Parallelism

Definition

Dealing with many things at once (interleaved)

Doing many things at the same instant

Needs

One thread + task switching

Multiple cores/threads running simultaneously

Node default

Yes — via the async event loop

No — single thread runs your JS

Analogy

One chef juggling several dishes

Several chefs each cooking a dish

Node is concurrent by default but not parallel — one thread interleaves many async tasks
The distinction is the key to understanding Node. **Concurrency** means making progress on multiple tasks by interleaving them — while one request waits on the database, the single thread handles another. **Parallelism** means literally executing multiple tasks at the same physical moment, which requires multiple CPU cores doing work simultaneously. Node's event loop gives you excellent *concurrency* on one thread: it never sits idle waiting on I/O, it switches to other work. But your JavaScript runs on *one* thread, so two functions never execute truly in parallel — for that you need workers, child processes, or clustering.
How one thread serves thousands of connections

Text
Single thread + event loop + async I/O:

  Request A ──▶ start DB query ──┐ (thread is FREE while DB works)
  Request B ──▶ start API call ──┤ (thread is FREE while API responds)
  Request C ──▶ start file read ─┤ (thread is FREE while disk reads)
                                 │
  DB result ready ◀──────────────┘ thread picks it up, finishes A
  API result ready ◀──────────────  thread picks it up, finishes B

  → The thread is only busy *dispatching* and *processing results*,
    never *waiting*. That's how one thread handles 10,000 connections.
I/O doesn't block the thread — Node offloads it and processes the result when it's ready
When Node starts an I/O operation (a query, an HTTP call, a file read), it hands the work to the OS or its [libuv](/nodejs/event-loop) thread pool and *immediately moves on* to other tasks — the JavaScript thread doesn't sit and wait. When the operation completes, its callback is queued and the event loop runs it. Because real servers spend most of their time *waiting* on I/O, a single thread that never blocks on waits can juggle enormous numbers of concurrent connections cheaply. This is why Node is superb for I/O-bound workloads — and why a CPU-bound task, which *does* occupy the thread, is so damaging.
The limits of one thread
CPU-bound work and multi-core usage are the two things one thread can't do — that's when you need workers/cluster
The single-threaded model has two hard limits. First, **CPU-bound work** — heavy computation, large crypto, image processing, parsing huge payloads — *occupies* the thread, so while it runs no other request makes progress and latency spikes across the board ([profiling](/nodejs/profiling) and a flame graph reveal these). Second, **multi-core machines** — one Node process uses one core, so a 16-core server runs at ~6% capacity with a single process. Neither is solved by async; async only helps with *waiting*, not with *working*. The remedies are parallelism: [worker threads](/nodejs/worker-threads) for CPU work inside one process, the [cluster module](/nodejs/cluster-module) to run a process per core, and [child processes](/nodejs/child-processes) for separate programs.
Choosing the right tool

Tool

Gives you

Best for

Memory model

Async/await

Concurrency, no parallelism

I/O-bound work (the default)

Shared (one thread)

Worker threads

Parallelism within one process

CPU-bound tasks (compute, crypto)

Isolated; share via SharedArrayBuffer/messages

Cluster

Parallelism across processes

Scaling an HTTP server across cores

Fully isolated processes

Child process

Run a separate program

Shelling out, isolating risky work

Fully isolated processes

Async for I/O, worker threads for CPU, cluster to use all cores, child processes to run other programs
A quick decision guide. If the work is **I/O-bound** (most web work), plain `async/await` is all you need — don't reach for threads. If it's **CPU-bound** and you want to keep it in-process sharing data efficiently, use [worker threads](/nodejs/worker-threads). If you want to scale a stateless **HTTP server** to use every core on the box, use the [cluster module](/nodejs/cluster-module) (or a process manager / multiple container replicas). If you need to run a **separate program** or isolate untrusted/risky work in its own memory space, use a [child process](/nodejs/child-processes). The rest of this section covers each in depth.
Processes vs threads
Threads share memory and are lighter; processes are isolated and more robust — pick by your needs
The deeper trade-off is **isolation vs efficiency**. *Threads* (worker threads) live in the same process and can share memory via `SharedArrayBuffer`, making data exchange fast and startup cheap — but a crash or memory corruption can affect the whole process, and they share the same memory limit. *Processes* (cluster workers, child processes) are fully isolated: a crash in one doesn't take down the others, each has its own memory and V8 instance, communication happens by message passing — but they use more memory and inter-process communication is slower. Choose threads when you need shared-memory speed within a trust boundary; choose processes when you need fault isolation and robustness.
Next
Run and control other programs from Node: [Child Processes](/nodejs/child-processes).