Concurrency and parallelism get used as synonyms, and most of the confusion about goroutines, threads and async code comes from that. They are different ideas, they solve different problems, and a program can have either one without the other. The video above walks through it on a whiteboard; this is the written version with Go examples you can run.
The one-sentence difference
Concurrency is about structure: dealing with many things at once. Parallelism is about execution: doing many things at once.
Concurrency is a property of your program’s design. You split work into independent tasks that can make progress without waiting on each other, and you compose them. Whether those tasks actually run simultaneously is a separate question.
Parallelism is a property of the hardware and the runtime. If you have four cores and four runnable tasks, they can execute at the same instant. If you have one core, the most you can do is interleave them very quickly, which still looks like “at once” to a human but is not parallel.
Rob Pike’s formulation is the one worth remembering: concurrency is not parallelism, but concurrency makes parallelism easy to obtain.
A kitchen, not a highway
A single cook preparing three dishes is concurrent. They chop onions while water boils, stir a sauce while bread is in the oven. Only one pair of hands is working, but nothing sits idle waiting for the slowest step.
Three cooks each preparing one dish is parallel. Three pairs of hands, three things genuinely happening at the same moment.
Three cooks who all need the same knife are parallel hardware with a concurrency problem: the design forces them to wait on a shared resource, so the extra hands buy you very little.
What concurrency buys you
Most programs spend most of their time waiting: for the network, for the disk, for a database, for a user. A concurrent design lets the program do something useful during that wait instead of blocking.
func fetchAll(urls []string) []string {
results := make([]string, len(urls))
var wg sync.WaitGroup
for i, u := range urls {
wg.Add(1)
go func(i int, u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
results[i] = err.Error()
return
}
defer resp.Body.Close()
results[i] = resp.Status
}(i, u)
}
wg.Wait()
return results
}
Ten URLs fetched this way take roughly as long as the slowest single request rather than the sum of all ten. That speed-up has nothing to do with how many cores you have. Run it with GOMAXPROCS=1 and it is just as fast, because the goroutines spend almost all their time waiting on sockets, and the scheduler hands the single core to whichever one has data ready. This is concurrency paying off on I/O-bound work.
What parallelism buys you
Parallelism helps when the bottleneck is the CPU itself. Summing a very large slice, encoding video, hashing passwords: there is no waiting to hide, only arithmetic to do, so the only way to finish sooner is to do arithmetic on more cores at the same time.
func sumParallel(nums []int, workers int) int {
chunk := (len(nums) + workers - 1) / workers
partial := make(chan int, workers)
for w := 0; w < workers; w++ {
lo := w * chunk
hi := min(lo+chunk, len(nums))
go func(part []int) {
s := 0
for _, n := range part {
s += n
}
partial <- s
}(nums[lo:hi])
}
total := 0
for w := 0; w < workers; w++ {
total += <-partial
}
return total
}
This code is also concurrent (it is structured as independent tasks), but it only gets faster when those tasks run in parallel. With GOMAXPROCS=1 it is no quicker than a plain loop, and slightly slower because of the goroutine and channel overhead. With eight cores and workers = 8 it approaches an 8x speed-up on a large enough slice.
Why Go makes this confusing, and then easy
Go blurs the line on purpose. You write concurrent code with goroutines and channels, and the runtime decides how much parallelism to apply, up to GOMAXPROCS (which defaults to the number of CPUs). The same program is concurrent-but-sequential on one core and concurrent-and-parallel on sixteen, with no code changes.
That is the sense in which concurrency “makes parallelism easy”: if the program is already decomposed into independent pieces, giving it more cores is a runtime knob rather than a rewrite. The reverse is not true. A program written as one long sequential loop cannot be parallelised by adding cores; it has to be restructured first.
The trade-offs nobody mentions in the definitions
- Concurrency adds coordination. As soon as two tasks touch the same memory you need a mutex, a channel or an atomic, and you have introduced the possibility of races and deadlocks. Run your tests with
go test -race. - Parallelism has a ceiling. Amdahl’s law: if 10 percent of your work is inherently sequential, no number of cores gets you past a 10x speed-up. Measure before you add workers.
- More goroutines is not more speed. For CPU-bound work, more goroutines than cores just adds scheduling overhead. For I/O-bound work, the limit is usually the remote system, not your process; a semaphore (a buffered channel) to cap in-flight requests is often the first thing you add.
- Both can make a program slower. A tiny workload split across workers can take longer than the straight loop, because spawning and synchronising cost more than the work saved.
A quick decision guide
| Your bottleneck is | Reach for | What it changes |
|---|---|---|
| Waiting on I/O (network, disk, DB) | Concurrency | Latency: overlap the waits |
| Raw CPU work | Parallelism | Throughput: use more cores |
| Both, in a server | Concurrency per request, parallelism across requests | Go’s default model already does this |
| Nothing measurable | Neither yet | Profile first (go tool pprof) |
Takeaways
Concurrency is how you structure a program so that independent things do not wait on each other. Parallelism is whether those things execute simultaneously, which depends on cores and the runtime. In Go you write the former and get the latter for free when hardware allows. The design question is always the same: what is the program actually waiting on? Answer that, and the choice makes itself.



