Systems Engineering Concurrency Distributed Systems · July 2026

Concurrent Doesn't Mean Parallel.
And parallel doesn't mean fast.

"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." — Rob Pike
PD
Pradeep A. Dalvi
Principal Systems Architect · Distributed Systems & Payments
11 min read
20×
max speedup with 95% parallel code — regardless of how many cores you add
5%
serial fraction that sets that ceiling — the part no amount of hardware can touch
64B
CPU cache line size — the invisible unit causing false sharing in "parallel" code
how coherence cost scales per Gunther's Universal Scalability Law
01

Concurrency Is a Design Property

Concurrency describes how a program is structured to handle multiple things. It says nothing about whether those things happen simultaneously. A concurrent program manages multiple tasks whose progress can be interleaved — one task suspended while another makes progress, then resumed when its condition is met.

Think of a single engineer handling multiple production alerts. They get paged on alert A, start investigating, issue a log query that takes 30 seconds. Rather than staring at the terminal, they pick up alert B. They respond to B, the query returns for A, they resume the A investigation. One person. Multiple things in progress. No actual simultaneity — just structured interleaving that eliminates idle time. That's concurrency. The engineer is the CPU. No parallelism required.

Infographic illustrating Concurrency — one worker, structured interleaving.
Figure 1: Concurrency — one worker, structured interleaving.

This is why Node.js handles tens of thousands of concurrent connections on a single thread. It isn't parallel — one piece of JavaScript executes at a time. But the event loop is structured to never block waiting for I/O: the moment a socket read would block, it registers a callback and moves to the next ready event. The CPU is never idle because a slow operation is pending. Concurrency doing real work, without any parallelism at all.

02

Parallelism Is an Execution Property

Parallelism describes how a program executes. It requires multiple processing units — CPU cores, machines, GPU shader units — and means that multiple tasks are genuinely running at the same wall-clock instant. Not interleaved. Simultaneous.

A team of five engineers each handling a separate incident in separate rooms — that's parallelism. The incidents are truly being worked at the same moment. No scheduling, no turn-taking. The hardware scales throughput linearly — up to a limit we'll come to shortly.

Infographic illustrating Parallelism — multiple workers, simultaneous execution.
Figure 2: Parallelism — multiple workers, simultaneous execution.

This is what a CPU does with SIMD instructions: apply the same operation to 16 values in a single clock cycle. It's what a GPU does with thousands of shader cores running the same kernel on different data simultaneously. It's what a database does when it fans out a query across shards. Rob Pike's framing is precise: dealing versus doing. Structure versus execution. The distinction sounds pedantic until you've spent a week debugging a system where someone treated them as the same thing.

03

The Two-by-Two Nobody Puts in the Textbook

The two properties are orthogonal — you can have either without the other. Each quadrant has a different performance profile, a different correctness model, and a different class of bugs waiting inside it.

Concurrent · Not Parallel

One worker, many tasks

Node.js event loop, Python asyncio, Go on GOMAXPROCS=1, cooperative OS scheduling. Single thread handles many tasks by interleaving. Excellent for I/O-bound work — the CPU is rarely the bottleneck. Adding cores doesn't help unless you fork processes.

Concurrent + Parallel

Many workers, many tasks each

Go goroutines across all cores, Java virtual threads + ForkJoinPool, async I/O + worker thread pool. Maximum throughput. Also maximum coordination complexity — this is where most correctness bugs live. Amdahl's Law applies in full force here.

Neither

One worker, one task at a time

A synchronous single-threaded server, a bash pipeline, a simple CLI script. Predictable, debuggable, and correct by default. Underestimated as a deliberate architecture choice at moderate scale, where coordination overhead of the alternatives isn't worth it.

Parallel · Not Concurrent

Many workers, one task each

SIMD vectorisation, GPU kernel execution, naive fork with fully independent work, data parallelism on disjoint partitions. Only works for embarrassingly parallel problems — no shared state, no coordination. The moment tasks need to communicate, you need concurrency design too.

The most common mistake lives in the bottom-right quadrant: treating a problem as embarrassingly parallel by adding threads, when it has shared state that requires concurrency design. The result is parallel form with sequential performance — every thread immediately serialises on the shared resource. You've paid the cost of both models and received the benefits of neither.

Adding threads to a system with shared state doesn't create parallelism. It creates a queue in front of a lock — and gives that queue sixteen entrances instead of one.

04

Amdahl's Law: The Ceiling Parallelism Can't Escape

Assume you've correctly identified a parallel workload. You've added cores. You're measuring speedup. Here is the law that governs what you can actually achieve — and it has no sympathy for your hardware budget.

Amdahl's Law — the theoretical speedup limit
S(N) = 1 / ((1 - P) + P/N)

S  →  theoretical speedup over single-core baseline
N  →  number of processors
P  →  fraction of work that can be parallelised (0 to 1)

If P = 0.80 (80% parallelisable):
  N =  2  →  1.67×  |  N =  4  →  2.50×
  N =  8  →  4.00×  |  N =  ∞  →  5.00×  ← hard ceiling. Always.

If P = 0.95 (95% parallelisable):
  N =  2  →  1.90×  |  N =  8  →  5.93×
  N = 64  →  14.6×  |  N =  ∞  →  20×   ← ceiling at 5% serial fraction.

The serial fraction (1 - P) sets the ceiling. Always. Regardless of cores.
ceiling — 20% serial code (∞ cores)
10×
ceiling — 10% serial code (∞ cores)
20×
ceiling — 5% serial code (∞ cores)
100×
ceiling — 1% serial code (∞ cores)

The implication is uncomfortable: the serial fraction — not the parallel fraction — is what your architecture is bounded by. Every lock acquisition is a serial section. Every global counter, every shared queue, every checkpoint write is a serial section. Code that looks parallel but converges on a shared resource has a higher serial fraction than it appears, and a lower ceiling than you've planned for.

This is why you can throw 64 cores at a workload and get 8× speedup and wonder where the other 8× went. It went into the 10% of your code that can't be parallelised — the logging, the coordination, the final merge, the network call that everything else depends on completing before the next step begins.

05

The Coordination Tax

Amdahl's Law models your code as if the parallel parts were free and the serial parts were the only cost. In reality, parallelism itself has a cost: the work required to coordinate independent workers. This is the coordination tax, and it erodes your speedup even before the serial fraction takes its cut.

Locks as Hidden Serial Sections

Every mutex in a parallel critical path is a serial section in practice, not just in theory. When 16 threads contend on the same lock, 15 are waiting while 1 executes the protected block. That's 15 cores parked, doing nothing, consuming scheduling overhead. The lock doesn't just protect state — it serialises execution. The effective utilisation of your parallel hardware during that window is 1/16.

False Sharing: The Invisible Serialiser

Two threads. Entirely separate data. No shared variables. No locks. They still interfere — if their data shares a CPU cache line.

False sharing — serialisation with zero shared variables
  struct Counters {
      int thread_0_count;  // offset  0  ┐
      int thread_1_count;  // offset  4  ├─ same 64-byte cache line
      int thread_2_count;  // offset  8  │
      int thread_3_count;  // offset 12  ┘
  }

  Thread 0 writes thread_0_count.
  Thread 1 writes thread_1_count.
  They share no variable. They do not know each other exists.

  Hardware's view:
  Both writes hit the same cache line.
  MESI coherence protocol invalidates that line on every write.
  Each core waits for exclusive ownership before proceeding.
  16 threads × 1 shared cache line = sequential writes in parallel hardware.

  Fix: pad each counter to fill its own 64-byte cache line.
  Alignment is not premature optimisation — it is correctness at the hardware layer.

False sharing is endemic to naive parallel code and invisible without profiling. You've written code with zero shared state, and the hardware has serialised it anyway because the memory layout placed different workers' data on the same physical cache line. Padding structs to align on cache line boundaries is not a micro-optimisation — it's the difference between parallel code that scales and parallel code that runs slower than its sequential equivalent under high thread counts.

The Gunther Correction: When More Threads Make Things Worse

Amdahl's Law assumes coordination is free. Neil Gunther's Universal Scalability Law adds the cost back in: a contention coefficient (serialisation from shared resources) and a coherence coefficient (the cost of keeping shared state consistent across cores). At high thread counts, the coherence cost grows as N² — quadratically. Beyond some inflection point, adding more threads actively slows the system down.

This is superlinear slowdown. It's real. It shows up in lock-heavy code under high parallelism, in databases with too many connections, in distributed systems with too many nodes writing to shared coordination state. The chart that was supposed to show linear speedup bends over and starts going the wrong direction. Adding hardware makes the system slower — not because the hardware is wrong, but because the design assumed coordination was free.

06

I/O-Bound vs CPU-Bound: The Decision That Precedes Architecture

Most systems are either waiting for something (I/O-bound) or computing something (CPU-bound). The model you choose should follow directly from which one they are. Choosing the wrong model for the workload type is the root cause of most of the complexity described above.

I/O-Bound workloads — concurrency is the answer
  • The CPU is idle most of the time — waiting for network, disk, database, or external API. The bottleneck is latency on external calls, not compute cycles.
  • Concurrency eliminates that idle time by interleaving tasks. A single thread can handle hundreds of concurrent I/O operations if structured correctly.
  • Parallelism adds marginal benefit — you need more cores only if CPU processing between I/O calls is itself significant.
  • Right models: async/await, event loops (Node.js, nginx), non-blocking I/O with a small thread pool, Go goroutines for network services.
  • Wrong model: one thread per connection — wastes memory on stack allocation and OS scheduling for threads that spend 99% of their time blocked.
CPU-Bound workloads — parallelism is the answer
  • The CPU is fully utilised — encoding video, running ML inference, compiling code, computing hashes. The bottleneck is compute cycles, not waiting.
  • Concurrency alone doesn't help: if you have one core and two CPU-intensive tasks, interleaving them takes twice as long as running one sequentially.
  • Parallelism across multiple cores or machines is the only way to increase throughput — up to the Amdahl ceiling set by your serial fraction.
  • Right models: worker thread pools sized to core count, process-level parallelism, SIMD for tight loops, distributed map-reduce for large data.
  • Wrong model: async I/O frameworks — structured for I/O interleaving, they add overhead without benefit when the bottleneck is pure compute.
Model
Type
Best for
Examples
Event loop (single-threaded async)
Concurrent
High-connection I/O services
nginx, Node.js, Redis
Thread-per-task, blocking I/O
Parallel
Request isolation, mixed I/O + CPU
Apache httpd, Java servlets
Goroutines / virtual threads
Both
High-throughput network services
Go, Java 21, Erlang
Worker pool + async I/O
Both
Mixed I/O-bound + CPU-bound work
Tokio (Rust), Node.js worker_threads
Data parallelism / SIMD
Parallel
Embarrassingly parallel compute
ML inference batching, video encoding
Actor model
Both
Distributed state, fault isolation
Erlang/OTP, Akka, Pony
Sequential single-threaded
Neither
Low volume, correctness-critical
Ledger writes, ordered event processors

Every architecture decision here should start with two questions, answered in order. First: where is the bottleneck? CPU, I/O, memory bandwidth, or network? The answer determines the model. I/O-bound systems need concurrency. CPU-bound systems need parallelism. Systems that are both need both — designed in layers, not blended into the same component.

Second: what does it cost to coordinate? Every piece of shared state between concurrent or parallel workers is a serial section waiting to happen. Identify it before you design the parallelism, because the shared state is where Amdahl's ceiling lives and where false sharing hides.

Concurrent means structured to interleave. Parallel means structured to simultaneously execute. Every system you build has a bottleneck that tells you which one it needs. The question is whether you listen before you design — or after you debug.

If you lead engineering teams, design distributed systems at scale, or find yourself thinking about building scalable systems — I’d love to connect.

linkedin.com/in/pradeep  ·  Pune, India