🧠 Python DeepCuts — 💡 Async/Await Under the Hood
Posted on: January 28, 2026
Description:
async and await are often introduced as a way to “run things in parallel,” but that mental model is misleading.
Async in Python is not about threads or CPU parallelism — it’s about how execution is scheduled.
This DeepCut explains what actually happens when you write async code and why it behaves the way it does.
🧩 What async def Really Creates
Calling an async function does not execute it.
async def greet():
return "Hello async"
coro = greet()
Instead, Python returns a coroutine object.
This object represents pending work, not running work.
Execution begins only when the coroutine is:
- awaited
- scheduled by the event loop
This is why simply calling an async function does nothing by itself.
🧠 What await Actually Does
await is not a blocking operation.
await asyncio.sleep(1)
It tells the event loop:
“Pause this coroutine here, and let something else run.”
This is called cooperative multitasking:
- tasks voluntarily yield control
- no OS-level preemption
- no thread switching
The event loop decides what runs next.
🔄 Coroutines vs Generators
Coroutines are implemented as state machines, similar to generators.
sample().__await__()
Internally:
- await works by iterating over an awaitable
- coroutines expose await()
- execution resumes when the awaitable signals completion
This explains why async feels lightweight compared to threads.
🧠 The Event Loop Is the Scheduler
The event loop is responsible for:
- running coroutines
- suspending them at await points
- resuming them when I/O is ready
await asyncio.gather(task1(), task2())
Multiple coroutines can make progress in the same time window — even on a single thread.
This is why async I/O scales well.
🧬 Async Is Not Parallelism
Async does not bypass the GIL.
async def cpu_bound():
for i in range(10_000_000):
pass
CPU-bound async code:
- blocks the event loop
- prevents other tasks from running
- offers no speedup
Async improves throughput for I/O-bound workloads, not CPU-heavy ones.
🔍 Tasks vs Coroutines
A coroutine is just a definition of work.
A Task is a coroutine scheduled on the event loop.
task = asyncio.create_task(work())
Tasks allow the event loop to:
- track execution
- cancel work
- manage lifecycle
Most frameworks automatically wrap coroutines in tasks for you.
🧠 Why Async Scales for I/O
Async excels when programs spend time:
- waiting on networks
- waiting on databases
- waiting on files
- waiting on timers
Instead of blocking threads, async:
- suspends coroutines
- keeps the event loop active
- overlaps waiting periods efficiently
This is how async servers handle thousands of concurrent connections.
⚠️ Common Async Misconceptions
- Async ≠ multithreading
- Async ≠ multiprocessing
- Async ≠ faster CPU execution
Async is a scheduling model, not a performance shortcut.
Understanding this distinction prevents poor architectural choices.
✅ Key Points
- async def returns a coroutine object
- await suspends execution cooperatively
- The event loop schedules coroutines
- Async runs on a single thread
- Async is ideal for I/O-bound workloads
- CPU-bound work still needs multiprocessing or native extensions
Async/Await is powerful — when used for the right problems.
Code Snippet:
import asyncio
import inspect
async def greet():
return "Hello async"
coro = greet()
print(coro)
print("Is coroutine:", inspect.iscoroutine(coro))
async def task():
print("Start task")
await asyncio.sleep(1)
print("Resume task")
async def one():
await asyncio.sleep(1)
print("One done")
async def two():
await asyncio.sleep(1)
print("Two done")
async def cpu_bound():
total = 0
for i in range(10_000_000):
total += i
return total
async def work():
await asyncio.sleep(1)
return "done"
async def main():
await task()
await asyncio.gather(one(), two())
await cpu_bound()
t = asyncio.create_task(work())
result = await t
print("Task result:", result)
asyncio.run(main())
No comments yet. Be the first to comment!