Process versus Thread
Processes buy isolation at the price of expensive creation, expensive switching and explicit communication; threads buy cheap sharing at the price of shared failure — and every runtime, browser and database picks a point on that line for a reason.
The problem
The comparison
The table below is the whole lesson in one screen. Every row follows from one fact: threads share an address space and processes do not. Sharing makes communication and switching cheap and makes failure and corruption shared; separation makes them expensive and contained. Numbers are typical Linux x86-64 figures, order-of-magnitude only; measure on your platform before quoting them.
| Dimension | Process | Thread |
|---|---|---|
| Address space | Private; shares only read-only code pages and explicit shared memory | Shared with all threads of the process |
| Isolation | A wild pointer faults in its own process only; kernel enforces boundaries | None: any thread can corrupt any other’s data |
| Communication | Explicit IPC: pipes, sockets, shared memory, message queues — copies or setup cost | Pass a pointer; synchronise with mutexes/atomics |
| Failure impact | A crash kills one process; a supervisor restarts it | A crash (segfault, uncaught exception) kills the whole process |
| Switching cost | ~2–5 µs + page-table switch, TLB flush unless tagged (PCID/ASID), cold caches | ~1–2 µs, same page tables, TLB kept warm |
| Creation cost | fork ~50 µs–1 ms (scales with page-table size); exec adds loader time | ~10–50 µs; a stack mapping and a task record |
| Memory per unit | Full private data + page tables (MBs); code shared | One stack (8 MB virtual, few kB committed) + ~10 kB kernel state |
| Security boundary | Yes: different uid, namespaces, seccomp possible per process | No: all threads run with the process’s credentials |
| Debug/observe | Own PID, own /proc entry, own limits, restartable | TID under the process; shares limits; cannot be restarted alone |
Isolation and the failure domain
Isolation is enforced by the MMU. A process that dereferences a bad pointer takes a page fault, the kernel finds no mapping, and sends SIGSEGV to *that* process. Its siblings never notice. That is why Chrome pays for a process per site: a renderer exploited by one page cannot read another page’s cookies from memory, and a renderer crash shows a sad tab, not a dead browser. It is why nginx and PostgreSQL survive a crashing worker with one reconnect rather than an outage.
Threads have no such wall. A segfault in one thread is a signal to the process; an uncaught exception terminates the process; a thread that corrupts the heap allocator’s metadata takes down whatever thread next calls malloc. Memory-safe languages remove the corruption but not the shared fate: an out-of-memory error or a stuck lock in one Java thread affects every thread in the JVM. The rule of thumb: put things that must not take each other down in separate processes, and things that must share large state cheaply in threads.
Communication and switching
Between threads, communication is a memory write plus a synchronisation primitive: a lock-protected queue, an atomic flag, a condition variable. Handing a 1 MB buffer to another thread costs a pointer. Between processes it costs a copy through the kernel (pipe, socket, message queue — see IPC: Deliberate Holes in Process Isolation) or the up-front work of mapping a shared-memory segment (Shared Memory: Zero Copies, Zero Protection) and then the *same* synchronisation problems as threads, because shared memory is shared memory.
Switching between two threads of the same process keeps the page tables and the TLB; switching between processes loads a new page-table root, which on x86 flushes the TLB unless the CPU tags entries with a process-context id (PCID, and ASIDs on ARM), and either way the new process starts with cold caches. The direct cost difference is small (a microsecond or so); the indirect cost — TLB misses and cache misses in the first milliseconds after the switch — is what shows up in benchmarks. See Context Switching.
- Same-process switch: registers + stack pointer; TLB warm. Cross-process: also
CR3/TTBR reload; TLB cold unless tagged. - Windows: switching between threads of different processes carries the same page-table cost; the thread is still the unit.
How real systems choose
Browsers: process per site for security isolation, threads inside each renderer for parsing, layout, JavaScript and compositing. nginx: one process per core, each running a single-threaded event loop, because the work is I/O-bound and the workers share nothing but the listening socket. PostgreSQL: process per connection, historically for robustness and portability, with explicit shared memory for the buffer pool — the reason PostgreSQL connections are expensive and everyone runs a pooler. MySQL, SQL Server, most JVM services: threads, because the buffer pool and caches are simply the heap.
Node.js: one process runs one event loop; to use eight cores you run eight processes (cluster) or worker threads with separate V8 isolates that share nothing by default. CPython: threads for I/O concurrency, processes for CPU parallelism because of the GIL in the default build — see How C++, JavaScript and Python Map onto the OS. Go, Java virtual threads, Erlang: one process, a few kernel threads, millions of cheap user-level tasks. None of these is the "right" answer; each is a position on the isolation-versus-sharing line chosen for its workload.
Key points
- Every difference between processes and threads follows from whether the address space is shared.
- Processes: isolation enforced by the MMU, explicit IPC, ~µs-to-ms creation, page-table switch on context switch, own credentials and limits.
- Threads: pointer-passing communication, ~10–50 µs creation, cheaper switches with a warm TLB, shared credentials — and a crash or heap corruption in one kills all.
- Put things that must not take each other down in separate processes; put things that share large mutable state in threads.
- Chrome, nginx, PostgreSQL, MySQL, Node and Go each sit at a deliberate point on this line; the figures are platform-dependent — measure before quoting.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why does a thread crash kill the whole process?
Because the kernel’s unit of protection is the address space; a corrupted heap or a fatal signal has no smaller container to be confined to.
▸Why is a process switch more expensive than a thread switch?
Switching the page-table root invalidates or re-tags the TLB and leaves the caches full of the old process’s lines; the extra direct cost is small, the indirect miss cost is not.
▸Why do databases disagree on this?
Workload and history: PostgreSQL chose per-connection processes for robustness on 1990s Unix and kept them; MySQL chose threads and a shared heap. Both then built the missing piece — shared memory for one, careful locking for the other.
Process or thread?
| Dimension | Process | Thread ✓ |
|---|---|---|
| Address space | Own page tables | Shared with siblings |
| Isolation | Strong — kernel enforced | None — same memory |
| Communication | IPC: pipes, sockets, shm | Shared memory + locks |
| Failure impact | One dies, others live | One crashes, all die |
| Switching cost | ~2–5 µs + TLB/cache flush | ~1–2 µs, same page tables |
| Creation cost | ~100 µs–1 ms (fork) | ~10–50 µs |
| Memory per unit | MBs: page tables + private copies | KBs: stack + TLS |
How it fails
What the failure looks like from inside real software.
- A multi-threaded service dies entirely because one request handler dereferenced null; the same bug in a process-per-worker design costs one request.
- A PostgreSQL server with
max_connections = 2000runs out of memory: each connection is a process with its own private memory, not a thread with a small stack. - A Node service "uses only one core": one process, one event loop; the fix is
clusteror worker threads, not more async. - Two processes share a memory segment and the developer assumes "processes are isolated, no locks needed" — the shared segment races exactly like threads do.
- A thread pool sized to 200 on a 4-core box spends more time in context switches and cache misses than in work; throughput drops as threads are added.