Processes vs. Threads
A process is an isolated address space with its own resources. A thread is an execution stream inside a process that shares memory with its siblings. That single difference drives isolation, cost, and bug classes.
Process
- Own virtual address space, file descriptors, heap, globals.
- Crash usually kills only that process.
- Communication via IPC: pipes, sockets, shared memory, files.
- Creation is relatively expensive (page tables, address space).
- Scheduling unit for the OS; can use multiple cores.
import { fork } from "node:child_process";
const child = fork("./worker.js");
child.send({ job: "resize", id: 42 });
child.on("message", (result) => console.log(result));
Use processes when:
- you need crash isolation (one tenant/job cannot corrupt another);
- you want to use multiple cores in a runtime with a GIL or single-threaded loop;
- memory leaks should be reclaimable by killing the process;
- security boundaries matter (
seccomp, separate users, containers).
Cost: IPC serialisation, duplicated memory, slower startup, more operational overhead.
Thread
- Shares heap and globals with other threads in the process.
- Has its own stack, registers, and program counter.
- Context switches are cheaper than process switches, but not free.
- A crash/segfault can take down the entire process.
- Needs synchronisation for shared mutable state.
import { Worker, isMainThread, parentPort } from "node:worker_threads";
if (isMainThread) {
const worker = new Worker(new URL(import.meta.url));
worker.on("message", (msg) => console.log("result", msg));
worker.postMessage({ n: 40 });
} else {
parentPort!.on("message", ({ n }) => {
parentPort!.postMessage(fib(n)); // CPU-bound work off the main loop
});
}
Threads are ideal for CPU-bound work that must not block the event loop, or for languages whose concurrency model is thread-per-request with blocking I/O.
Danger: shared mutable state. Without a lock or ownership discipline, you get data races that are timing-dependent and hard to reproduce.
Context switching
The OS saves/restores registers and switches stacks; the CPU cache may be invalidated. Costs:
| Switch | Approx. cost | Notes |
|---|---|---|
| Same-core thread switch | ~1–5 µs | cache effects dominate |
| Cross-core thread switch | higher | cache migration, TLB |
| Process switch | higher still | address space switch (TLB flush) |
| Async task switch (event loop) | ~100s ns | no kernel involvement |
At 100,000 switches/s, even 2 µs each is 20% of a core. This is why thread-per-connection collapses at high concurrency and why async runtimes exist.
Scheduling
The OS scheduler preempts threads based on priority and fairness. You cannot rely on execution order across threads. Implications:
- "Thread A usually runs first" is not a guarantee;
- busy-waiting starves others;
- priority inversion can occur with locks;
- CPU affinity can help latency-sensitive workloads but narrows scheduling options.
IPC
| Mechanism | Direction | Notes |
|---|---|---|
| Pipe | one-way byte stream | simple, local |
| Unix domain socket | bidirectional | fast, permissioned via filesystem |
| TCP socket | bidirectional | works across hosts |
| Shared memory | bidirectional | fastest, requires synchronisation |
| Message queue | async | decouples producers/consumers |
| RPC | request/response | adds stubs/serialisation |
Prefer copying messages over shared memory unless profiling proves the copy is the bottleneck. Shared memory reintroduces every race condition processes were meant to avoid.
Event loop (single-threaded concurrency)
Node.js and browsers run JavaScript on one thread. Concurrency comes from non-blocking I/O and the event loop:
async function handle(req: Request): Promise<Response> {
const [user, orders] = await Promise.all([
users.find(req.userId), // I/O
orders.list(req.userId), // I/O
]);
return json({ user, orders });
}
Rules:
- Never block the loop with CPU-heavy work (hashing, image processing, large JSON) — move it to a worker/process.
- Async/await does not create threads; it interleaves tasks on one thread.
- All callbacks share one thread: a slow handler delays everyone.
Green threads and goroutines
Runtimes can multiplex many lightweight tasks onto few OS threads (Go goroutines, Java virtual threads, Erlang processes). Benefits:
- cheap creation (KB-scale stacks),
- blocking-style code without one OS thread per task,
- runtime scheduler handles I/O readiness.
Costs:
- still shared memory in Go/Java → races remain;
- goroutine leaks (blocked forever) are a real failure mode;
- debugging stack traces across thousands of tasks is harder.
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
process(n)
}(i)
}
wg.Wait()
}
Choosing
| Workload | Choice |
|---|---|
| I/O-heavy API server (Node/Python) | event loop + worker threads for CPU |
| CPU-bound batch jobs | process pool / worker threads |
| Crash-isolated plugins/tenants | processes or containers |
| Massive concurrent connections | event loop, goroutines, or virtual threads |
| Shared cache/graph in memory | threads with locks, or actors |
| Untrusted code | separate process/container |
Failure modes
- Goroutine/thread leak: tasks block forever (missing timeout/cancel).
- Unbounded spawning: one task per request without a pool → memory/FD exhaustion.
- Blocking the event loop: p99 spikes for all requests.
- Process-per-request: startup cost dominates.
- Shared memory without locks: silent corruption.
- Copying large payloads over IPC: throughput collapse.
Interview drill
1. "Why is Node single-threaded, and how does it handle 10k connections?"
- Cover: event loop + non-blocking I/O; one thread for JS; worker threads/processes for CPU; backpressure; connection caps.
- Follow-up: "What happens with a CPU-heavy request?" (blocks everyone; offload it).
- Common miss: confusing non-blocking I/O with multithreading.
2. "Threads or processes for untrusted plugins?"
- Cover: process/container isolation; crash containment; resource limits; IPC cost; shared cache implications.
- Follow-up: "How do you pass data between them?" (serialised IPC, not shared memory).
- Common miss: running untrusted code in threads of the main process.
3. "What is a context switch and why does it matter at scale?"
- Cover: register/stack switch, cache/TLB effects; cost × rate; async tasks cheaper; measure switches and latency.
- Follow-up: "How would you detect switch overhead?" (perf/scheduler stats, throughput vs concurrency).
- Common miss: assuming more threads always mean more throughput.
Checklist
- The unit (process/thread/task) matches isolation and CPU needs.
- CPU-heavy work is off the event loop.
- Concurrency is bounded by pools/limits.
- Shared memory is protected or avoided.
- IPC payloads are sized and serialised deliberately.
- Tasks have timeouts/cancellation and cannot leak.
- Shutdown drains in-flight work.
Next: Synchronization & Locks — the primitives and their costs.