Skip to content

Async & Concurrency — Overview

Coming from JavaScript’s single-threaded world

Section titled “Coming from JavaScript’s single-threaded world”

JavaScript runs in a single thread. There is one call stack, one event loop, and no shared memory between concurrent units of work. When you write async/await in Node.js you are scheduling microtasks on that single thread — you never run two things truly at the same time inside one process.

Python gives you three different concurrency tools and you have to choose the right one for the job:

ToolWhat it isBest for
asyncioSingle-threaded cooperative concurrencyI/O-bound async code (HTTP, DB, files)
threadingOS threads sharing one interpreterI/O-bound code, legacy blocking libraries
multiprocessingSeparate OS processesCPU-bound work that needs true parallelism

These two words are often conflated. They mean different things:

  • Concurrency — multiple tasks are in progress at the same time, but not necessarily running at the exact same instant. The event loop switches between tasks when one is waiting. This is how Node.js works, and how Python’s asyncio works.
  • Parallelism — multiple tasks execute at the exact same instant on different CPU cores. This requires multiple OS processes in Python (or multiple threads in most other languages).
TypeScript
// JavaScript/Node.js
// Concurrency via the event loop — single thread, never truly parallel
async function fetchUser(id: number): Promise<User> {
// While this awaits, other promises can run
return await db.query("SELECT * FROM users WHERE id = $1", [id]);
}
// Promise.all is concurrent, not parallel — still one thread
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1),
]);
Python
# Python
# asyncio — same single-threaded cooperative model as JS
import asyncio
async def fetch_user(user_id: int) -> dict:
# While this awaits, other coroutines can run
await asyncio.sleep(0) # yield to event loop
return {"id": user_id}
# asyncio.gather — same idea as Promise.all
async def main() -> None:
user, posts = await asyncio.gather(
fetch_user(1),
fetch_posts(1),
)
TypeScript
// JavaScript has one model: event loop + Web Workers for threads
// One thread for JS logic, workers for CPU work (separate memory)
// Main thread
const worker = new Worker("worker.js");
worker.postMessage({ data: bigArray });
worker.onmessage = (e) => console.log(e.data);
Python
# Python has three models
import asyncio # like Node's event loop
import threading # OS threads (limited by GIL — see below)
import multiprocessing # true parallelism, separate memory
# Pick based on your bottleneck:
# waiting on network/disk → asyncio or threading
# heavy CPU math → multiprocessing
flowchart TD
  io{"Bottleneck is waiting on I/O? (network, disk, DB)"}
  cpu{"Bottleneck is CPU computation? (math, image, ML)"}
  simple{"Just want a simple high-level API?"}
  io -- Yes --> asyncio["Use asyncio (preferred) or threading"]
  io -- No --> cpu
  cpu -- Yes --> mp["Use multiprocessing (bypasses the GIL)"]
  cpu -- No --> simple
  simple -- Yes --> cf["Use concurrent.futures (wraps threading and multiprocessing)"]
Choosing a concurrency tool

The rest of this module covers each tool in depth.

What is the key difference between concurrency and parallelism?
Which Python tool should you reach for when doing CPU-bound work that needs true parallelism?
Why do Python threads NOT achieve parallelism for CPU-bound tasks?
Which JavaScript concept is most analogous to Python's asyncio?