Concurrency vs Parallelism: The Distinction Most Engineers Get Wrong
Hook #
Here's a question most software engineers fail despite having years of experience:
Is a single-threaded JavaScript runtime, handling 10,000 concurrent HTTP requests via async/await, **concurrent* or parallel?*
If your instinct is "both" or "I'm not sure," you're in good company — and you're about to fix that. The answer is: concurrent, not parallel. JavaScript runs on a single thread; only one piece of code executes at any moment. But it structures its work to handle many tasks in interleaved fashion. Concurrency and parallelism aren't synonyms; they're orthogonal axes. The distinction is one of the single most useful concepts in modern systems engineering, and most CS courses miss it. This lesson is the fix.
What you'll be able to do by the end of this lesson #
- State the precise definition of concurrency (the structure of dealing with many things at once) vs parallelism (the execution of many things at once).
- Identify whether a given system is concurrent, parallel, both, or neither.
- Articulate Rob Pike's framing: concurrency is composing independently executing computations.
- Recognise four real systems and classify each on both axes.
- Explain why concurrency without parallelism is still a useful structuring tool.
A quick try before we start #
For each of these systems, decide: concurrent? parallel?
- Python with the GIL, running multiprocessing pool of 8 workers.
- JavaScript Node.js, single-threaded event loop, handling 10,000 HTTP requests via async/await.
- C program with one thread, no async, executing
for (i = 0; i < 1_000_000; i++). - Elixir running on a 16-core machine with 1 million BEAM processes.
Predict before reading on. The answers separate "engineers who use these tools" from "engineers who understand them."
The core idea, from first principles #
Definitions #
Concurrency is a structural property of a program. A concurrent program is organised as multiple independently-progressing tasks (functions, threads, processes, fibres, actors — the specific construct doesn't matter). The tasks can be in various states of progress at any given moment.
Parallelism is an execution property of a running program. A parallel execution is one where multiple operations actually happen at the same time, on different hardware (CPU cores, GPUs, distributed machines).
The distinction:
- Concurrency is about handling many tasks.
- Parallelism is about executing many tasks simultaneously.
You can have one without the other.
The four-quadrant table #
| Not concurrent | Concurrent | |
|---|---|---|
| Not parallel | Sequential single-threaded program (Quadrant A) | JavaScript event loop; Python with GIL + asyncio (Quadrant B) |
| Parallel | Vectorised SIMD; GPU shader; HPC numerical loops (Quadrant C) | Multi-threaded server; Erlang/Elixir on multi-core (Quadrant D) |
Quadrant A: Not concurrent, not parallel. A single function calling another in sequence. No interleaving, no simultaneity. Most "main" code.
Quadrant B: Concurrent, not parallel. The program is structured as multiple tasks (event handlers, fibres, asyncio coroutines) but executed by one CPU at a time. JavaScript event loops. Python's asyncio (without multiprocessing). Single-threaded actor systems. This is the trickiest quadrant for engineers to recognise — the program acts concurrent (handling many requests "at the same time") but doesn't use parallel hardware.
Quadrant C: Not concurrent, parallel. A single computation that uses parallel hardware. A matrix multiplication compiled to SIMD instructions. A GPU shader doing the same operation on every pixel. No task structure; just a single computation that maps to parallel hardware.
Quadrant D: Concurrent, parallel. Both. Multi-threaded web servers. Elixir on a multi-core machine. Most modern production systems. The program is structured as multiple tasks AND executes them on different cores.
The mental shift #
Pike's most-cited articulation: "Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once."
The verbs matter. "Dealing with" is structural — how the program is organised to handle the workload. "Doing" is execution — what the CPUs actually do per nanosecond.
This means: you can design a concurrent program that runs sequentially. You write event handlers, futures, coroutines, channels — but the runtime executes one at a time. The program is structured for concurrency; it doesn't execute with parallelism.
Conversely: you can have parallel execution without concurrent structure. A vectorised loop running on a GPU has no "tasks" — it's a single computation done in parallel. The program is not structured as concurrent (no event handlers, no futures); it executes with parallelism.
This is why the engineering question "should this be concurrent?" is a different question from "should this be parallel?" They're orthogonal. Different costs, different benefits, different design choices.
Why concurrency without parallelism is still useful #
The most subtle case: you write concurrent code on a single-threaded runtime. Why?
Responsiveness: while one task waits for I/O (network, disk, sleep), other tasks can run. A web server handling 10,000 simultaneous requests on a single thread — most of those requests are waiting for the database or the network; concurrency lets the server be responsive without idle waiting. Node.js makes this its main feature.
Resource isolation: concurrent tasks can fail independently. In Erlang/Elixir, each task is a process; if one crashes, the others continue. The actor model gives fault isolation even without parallelism.
Composition: concurrent code can be composed of simpler pieces. Each task is a self-contained piece of behaviour; the runtime composes them. This is the structural benefit — easier to reason about, easier to test.
These benefits don't require parallel execution. A single-threaded concurrent system can be responsive, fault-isolated, and modular — all without multiple cores.
Why parallelism without concurrency is also useful #
The mirror case: you write a parallel computation without concurrent structure.
Throughput on regular workloads: matrix multiplication, graphics rendering, scientific simulation. These have no "tasks" — just a uniform computation over data. Parallelism gives raw throughput; concurrency would just add overhead.
SIMD and GPU computing: hardware that does one operation on many data elements simultaneously. No tasks, no scheduling, no concurrency — just data-parallel execution.
This is why GPU programming feels different from multi-threaded CPU programming. The CPU is concurrent-by-design (multiple cores, each running tasks); the GPU is parallel-by-design (thousands of threads doing the same thing).
The answer to the warm-up #
- Python multiprocessing: concurrent (multiple processes) AND parallel (each process runs on a separate core).
- JavaScript with async/await: concurrent (multiple promises being managed) but NOT parallel (single thread).
- Sequential C loop: neither concurrent nor parallel. (Though the compiler might vectorise it — turning it into Quadrant C.)
- Elixir on 16 cores: concurrent (millions of BEAM processes) AND parallel (16 cores running them).
Why this works the way it does #
The deep reason this distinction is useful: the two properties have different costs and different benefits. Engineering them together when you could separate them often leads to bugs (concurrency without need = unnecessary complexity; parallelism without need = wasted hardware).
The historical reason: until ~2005, CPU clock speeds doubled every couple of years (Moore's law). Single-threaded code got faster automatically. After 2005, clock speeds plateaued; cores multiplied instead. Parallelism became necessary for performance gains. But concurrency had been useful long before that — for I/O-bound tasks (web servers, GUI event loops), responsiveness mattered even on single-core hardware.
The two trends converged in modern systems: multi-core machines need both concurrency (to structure work into tasks) and parallelism (to execute those tasks on different cores). But the concepts remained distinct, and engineering them correctly requires distinguishing them.
A third reason: the concurrency models you'll learn (actors, CSP, shared-memory) work the same way whether you have parallelism or not. An Elixir program runs the same way on 1 core and 64 cores — same code, same model, just different execution speed. The model is concurrent; the parallelism is opportunistic.
Explain it back to me #
A teammate writes: "I made my service multi-threaded so it's concurrent now."
In your own words, untangle this. Multi-threading might give both concurrency and parallelism — but the phrasing reveals confusion. What's the right way for them to talk about what they actually did?
And one more: a colleague is debugging a Node.js server that's "slow under load." They want to add cluster to "make it parallel." Is this useful? Why might it help? What would not be helped by going parallel?
A small exercise #
Three short tasks:
Classify each system on both axes (concurrent? parallel?): a. Rails server with Puma (multi-threaded) on a single-core VPS. b. Same Rails server on an 8-core VPS. c. A Python script that uses
multiprocessing.Poolto compute prime numbers on 4 cores. d. A GPU running CUDA on a deep-learning training loop. e. Erlang program with 1 process running on a 1-core machine.Write a sentence for each: when would you choose each combination? a. Concurrent but not parallel. b. Parallel but not concurrent. c. Both concurrent and parallel.
Identify the misuse. A teammate proposes using Python
threading(which still has the GIL — only one thread runs Python at a time) to "speed up CPU-bound number crunching." Explain why this won't help and what they should use instead.
Answers to (1)
a. **Concurrent, not parallel.** Multiple threads handling requests, but only one CPU. Concurrency for responsiveness; no parallelism. b. **Concurrent AND parallel.** Same code; multi-core hardware actually runs threads simultaneously. c. **Parallel, possibly concurrent.** Multiprocessing pool gives parallelism (separate cores). Whether "concurrent" depends on how you view it — each worker is its own process; they're not interleaved tasks within one program. d. **Parallel, not concurrent.** A single computation (training loop) distributed across thousands of GPU cores. No task structure. e. **Concurrent, not parallel.** Erlang structures the program as multiple tasks (processes); a 1-core machine can only execute one at a time. (3) The GIL means Python threads can't execute Python bytecode in parallel — only one thread holds the GIL at a time. *Threading* is useful for I/O-bound work (because I/O releases the GIL); it's useless for CPU-bound. The fix: `multiprocessing` (separate processes, separate Python interpreters, true parallelism) or write the hot loop in C/Cython/Rust.Where this connects #
Backward: Course 3.4 lesson 5 (BEAM lens) introduced the actor model and BEAM processes — a concurrency model that gives you concurrency and parallelism via the underlying VM. Course 1.3 lesson 1 (Why C) gave you the OS view of processes; this lesson is the abstract framing on top.
Forward: Lesson 2 (work-span model) gives you the quantitative framework for parallelism — when does adding more cores actually help? Lesson 3 (parallel patterns) covers the standard parallel algorithms (reduce, scan, MapReduce). Lesson 4 (concurrency models) covers three approaches to concurrency at the programming model level. Lesson 5 (lock-free + GPU) covers the advanced topics.
Course 7.x (Distributed Systems) generalises further — distributed systems are concurrent across machines, often with parallelism but always with concurrency-as-structure. The CAP theorem and consistency models live in that course; they presuppose the conceptual distinction this lesson teaches.
The zc-mapreduce-word-counter project for this course is a parallel pattern in production. After lesson 3 you'll have the framework to build it; after lesson 4 you'll see how the same problem could be structured with different concurrency models.
Carry forward one mental habit: when someone says "we made it concurrent," ask whether they mean structure or execution. The right answer is almost always "we restructured it to have multiple tasks — and we may or may not get parallelism depending on the runtime." The distinction shapes every design discussion about performance, scalability, and fault tolerance.
That's the free preview. Sign in to continue this course.
Sign in to continueNew here? Make a desk →