← All EduBytes

Async does not mean parallel

1 September 2026 · 4 min read

You made the function async, added await everywhere, and it runs at exactly the same speed. That is the expected result, and the reason is a distinction the word async never makes.

You had a function that took eight seconds. You made it async, put await in front of the slow calls, and ran it again. Eight seconds.

Nothing is broken. You changed how the work is structured, and the thing you wanted to change was how it executes. The word async never promised the second one, and the gap between those two ideas is one of the more expensive confusions in everyday programming.

Two words that are not synonyms

The cleanest statement of this comes from a talk Rob Pike gave in 2012: concurrency is dealing with many things at once; parallelism is doing many things at once.

Concurrency is about structure. It says your program has several independent strands of work that can be interleaved, so that when one is stuck the others can proceed. It is a property of how the program is written.

Parallelism is about execution. It says two things are physically happening at the same instant, which requires two things capable of executing — two cores, two machines.

A concurrent program can run on one core, and then nothing is parallel about it. A concurrent program on eight cores may run in parallel. async gives you the first. Only a runtime with multiple threads of execution gives you the second.

The question that predicts the answer

Before reaching for async, ask one thing about your slow code: is it waiting, or is it working?

Waiting — a network call, a database query, reading a file, a timer. Your CPU is doing nothing at all. It has issued a request and is sitting there. This is where async pays, and it pays enormously: while one strand waits, others run, and you can have thousands of outstanding waits at once on a single thread because a suspended task is a small object rather than an operating system thread with its own stack.

Working — resizing images, parsing a large document, running a simulation, any tight loop. The CPU is fully occupied. There is no idle time to reclaim. Async cannot help, because async’s entire mechanism is using the gaps, and there are no gaps. You need more cores, and reaching them requires something other than async.

Most disappointing async refactors are someone applying the waiting solution to a working problem.

What this means per language

The distinction shows up differently depending on what the runtime can do.

JavaScript has one thread running your code, with an event loop. async here can never produce parallelism — not in the browser, not in Node. It lets one thread stop wasting time on I/O, which is a great deal, and that is all it does. Actual parallelism requires Web Workers or worker threads, which are separate JavaScript environments that communicate by message passing.

This also gives JavaScript its sharpest failure mode: a single long synchronous computation blocks everything, because there is nothing else to run on. A two-second JSON parse in the middle of an async handler stalls every other request on that process.

Python runs asyncio on one thread, so the same applies. Threads exist but have historically been prevented by the global interpreter lock from executing Python bytecode simultaneously, so they help with I/O and not with computation — free-threaded builds are changing this, but they are not yet the default you should assume. For CPU-bound work the answer has long been multiprocessing, or pushing the loop into a library like NumPy that releases the lock and runs native code.

Go is the instructive contrast. Goroutines are concurrency, exactly like async — but the runtime multiplexes them onto a pool of OS threads sized to your cores, so concurrent Go code also becomes parallel with no change to how it is written. Same structure, different execution, and it is the runtime that decides which. Rust’s async is closer to Go here than to JavaScript, depending on the executor you choose.

That comparison is the point. The structure is portable; whether it turns into parallelism is a fact about the runtime underneath, not about the keyword.

The trap that catches everyone once

In an async runtime, one blocking call poisons the whole loop.

Call a synchronous library — a driver that does not know about async, a time.sleep, a file read on a path with no async version — inside an async function, and the event loop does not merely wait for you. It cannot run anything. Every other pending task on that thread is frozen until you return.

This is worse than the equivalent mistake with threads, where the scheduler would have preempted you, and it is why the symptom is so confusing: throughput collapses under load with no error, no exception, and a CPU that looks idle.

What to keep