Python
Async Programming
Threading
Multiprocessing
Concurrency

Is python threading or multiprocessing at core of async calls?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python async code is not built on multiprocessing, and it is not fundamentally built on threads either. The core idea behind asyncio is cooperative multitasking on an event loop, where coroutines voluntarily pause at await points so other work can run.

What Is Actually at the Core

At the core of Python async calls is the event loop. The loop tracks coroutines, timers, sockets, and other I/O readiness events, then resumes tasks when they can make progress.

That means a typical async program runs in a single OS thread by default. It is concurrent because tasks yield control explicitly, not because the interpreter is preemptively switching among worker threads.

A minimal example shows the model clearly.

python
1import asyncio
2
3async def fetch(name, delay):
4    print(f"start {name}")
5    await asyncio.sleep(delay)
6    print(f"done {name}")
7    return name
8
9async def main():
10    results = await asyncio.gather(
11        fetch("a", 1),
12        fetch("b", 1),
13        fetch("c", 1),
14    )
15    print(results)
16
17asyncio.run(main())

All three tasks overlap, yet no threads are required for the concurrency itself.

Why This Works Without Threads

The event loop cooperates with nonblocking I/O. When a coroutine awaits a network read, sleep, or another awaitable object, the loop can schedule another coroutine immediately.

This works well for:

  • HTTP clients and servers
  • database drivers with async support
  • websockets
  • message consumers
  • many tasks that spend most of their time waiting

That is why async programming is primarily an I/O-bound concurrency model.

Where Threads Do Enter the Picture

Threads become relevant when async code needs to call blocking functions that do not provide async APIs. The event loop can offload that work to a thread so the main loop stays responsive.

In modern Python, asyncio.to_thread is the simplest bridge.

python
1import asyncio
2import time
3
4def blocking_call():
5    time.sleep(2)
6    return "finished in thread"
7
8async def main():
9    result = await asyncio.to_thread(blocking_call)
10    print(result)
11
12asyncio.run(main())

The async system is still centered on the event loop. Threads are supporting infrastructure for blocking code, not the foundation of the model.

Some async libraries also use internal thread pools behind the scenes for DNS resolution, file operations, or compatibility layers. That does not change the conceptual core.

Where Multiprocessing Fits

Multiprocessing is useful for CPU-bound work, not as the basis of async I/O. If you have heavy numeric computation, image processing, or data parsing that saturates the CPU, an event loop alone will not help because the task does not yield while it is computing.

In that case, you can combine async orchestration with a process pool.

python
1import asyncio
2from concurrent.futures import ProcessPoolExecutor
3
4def cpu_heavy(n):
5    total = 0
6    for i in range(n):
7        total += i * i
8    return total
9
10async def main():
11    loop = asyncio.get_running_loop()
12    with ProcessPoolExecutor() as pool:
13        result = await loop.run_in_executor(pool, cpu_heavy, 1_000_000)
14        print(result)
15
16asyncio.run(main())

Here again, multiprocessing is an integration strategy, not the core mechanism behind async function calls.

Async Is Cooperative, Not Magical Parallelism

A common misconception is that async automatically makes code faster. It does not. It makes waiting efficient.

If a coroutine calls a blocking function directly, the whole event loop can stall.

python
1import asyncio
2import time
3
4async def bad_task():
5    time.sleep(2)
6    print("this blocked the loop")
7
8asyncio.run(bad_task())

That code uses async def, but it is not truly asynchronous because time.sleep blocks the thread. Replace blocking calls with async equivalents such as asyncio.sleep, async HTTP clients, or thread/process offloading where appropriate.

Common Pitfalls

The biggest mistake is assuming async means parallel CPU execution. It does not. A single event loop thread can juggle many waiting tasks, but CPU-heavy work still blocks unless moved elsewhere.

Another mistake is mixing blocking libraries into async code and then wondering why concurrency disappears. If the library has no async API, use to_thread or another executor strategy.

A third issue is comparing threads and async as if one must replace the other entirely. In practice, many systems use all three models: async for orchestration, threads for blocking adapters, and processes for CPU-bound stages.

Summary

  • Python async calls are centered on the event loop and coroutine scheduling.
  • Threading is not the core of async, though threads are often used to offload blocking functions.
  • Multiprocessing is useful for CPU-bound work, not for basic async I/O.
  • Async excels when tasks spend most of their time waiting on I/O.
  • Blocking calls inside a coroutine still block the event loop.
  • Real systems often combine async, threads, and processes for different kinds of work.

Course illustration
Course illustration