Async foundations: awaitables and scheduling

Native coroutines, await boundaries, event-loop control flow, and blocking hazards

`await` is a suspension point that pauses a coroutine so the event loop can run other tasks. Native coroutines, introduced in PEP 492 (Python 3.5), are defined with `async def`. Calling one creates a coroutine object without executing any code. Only `await` drives execution. CPython implements coroutines on the same frame-evaluation machinery as generators, using the `RESUME` opcode for suspension and resumption. The event loop schedules tasks cooperatively: a coroutine must reach an `await` point for other tasks to run. `asyncio.run()` (Python 3.7+) creates and manages the event loop. `time.sleep()` blocks the loop because it does not yield control. `asyncio.sleep()` is the correct non-blocking alternative. <a href="/async-context-backpressure">Learn backpressure and concurrency limiting for async code</a>. <a href="/async-iterators-generators">See async generators for lazy streaming</a>. <a href="/runtime-gil-performance">Compare async with threading and multiprocessing</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Every blocking boundary is everyone's problem.

:)
TABLE OF CONTENTS
6.2Async foundations: awaitables and scheduling

Native coroutines, await boundaries, event-loop control flow, and blocking hazards

await suspends your function until something else is ready. That is the core idea, but it takes time to internalize what suspension really means — your function pauses, the event loop runs other work, and control comes back to you when the awaited result is ready.

Think of await like ordering coffee at a busy café. You place your order (start the operation), step aside from the counter (suspend), and the barista calls your name when it is ready (resume). While you wait, other customers can order and receive their drinks. That is cooperative concurrency: you yield the counter willingly, trusting that you will be served when your drink is ready.

Core answer

The async keywords give Python a native way to express cooperative suspension.

  • async def defines a native coroutine function
  • calling it creates a coroutine object
  • await suspends that coroutine until another awaitable produces a result

The right mental model for async is cooperative suspension:

  • your coroutine runs
  • it reaches await
  • it suspends
  • the event loop can run something else
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+
import asyncio
async def fetch_flag():
await asyncio.sleep(0.1)
return "BR"
print(asyncio.run(fetch_flag()))
See Async Scheduling on a Timeline

Follow how native coroutines enter the event loop, suspend at await points, and resume when awaited work becomes ready.

A few definitions

Coroutine function ───────────────────────────────

  • declared with async def

Coroutine object ─────────────────────────────────

  • created when a coroutine function is called

Awaitable ────────────────────────────────────────

  • an object that can appear to the right of await

Event loop ───────────────────────────────────────

  • the scheduler that drives tasks and other awaitables forward when they become ready

The official docs use "awaitable" as the umbrella term because not everything awaited is the same kind of object. Coroutines, tasks, and futures all fit under that umbrella.

Guido's trick for reading async code

One of the easiest ways to read async code is to ignore the scheduling details first and read it almost as if it were synchronous, then come back and mark every await as a potential suspension boundary.

# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+
async def load_user_page(client, user_id):
profile = await fetch_profile(client, user_id)
orders = await fetch_orders(client, user_id)
return profile, orders

First reading:

  • load profile
  • load orders
  • return both

Second reading:

  • the coroutine may suspend at await fetch_profile(...)
  • then suspend again at await fetch_orders(...)

That second pass is where latency, concurrency, and failure behavior become visible.

New concept: awaitable

The language reference defines await in terms of the awaitable protocol. In practice, the main awaitables you touch in application code are:

  • coroutine objects
  • asyncio.Task
  • asyncio.Future
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+
import asyncio
async def child():
await asyncio.sleep(0.1)
return "ok"
async def main():
coro = child()
task = asyncio.create_task(child())
return await coro, await task
print(asyncio.run(main()))

The important design fact is that await is about protocol, not spelling. If an object supports the awaitable protocol, Python can suspend on it.

The secret of native coroutines

Native coroutines look like a completely new feature, but the mechanism did not appear from nowhere. await inherits much of its coroutine-driving behavior from generator machinery.

That helps explain the implementation lineage, but modern async code is more than "just generators." The key ideas are:

  • suspension and resumption
  • the event loop that drives the process

The official language-level contract is the async/await behavior itself. The exact internal steps, bytecode, and event-loop plumbing are interpreter and library implementation detail.

The all-or-nothing problem

A critical production constraint: asynchronous design is not a small cosmetic switch. If one critical layer blocks, the benefit collapses.

# [CURRENT - 3.10-3.14] Works on Python 3.x
import asyncio
import time
async def bad_worker():
time.sleep(0.5)
return "done"

That coroutine is syntactically async, but it is operationally blocking. It does not cooperate with the event loop while time.sleep is running.

This is why async systems need boundary discipline:

  • async-compatible network/database clients
  • explicit delegation for blocking calls
  • careful CPU-bound escape hatches

See and .

Version context

Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.

Important version markers:

  • async / await syntax: Python 3.5+ PEP 492
  • asyncio.run: Python 3.7+

Because this project targets current Python, native coroutine syntax is the baseline. Generator-based coroutine style is historical context, not a recommended production style.

Edge cases and gotchas

Calling an async def function does not run it immediately. It creates a coroutine object.

# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+
async def hello():
return "hi"
result = hello()
print(result)
result.close()

Another common mistake is treating await as if it always means "run in parallel." It does not. It means "suspend here until this awaitable completes."

A coroutine that does CPU work or blocking I/O without awaiting is worse than synchronous code hidden behind async syntax, because readers expect event-loop cooperation that never happens.

Production usage

Use this foundation model when reading or designing async code:

  1. mark every await
  2. ask what object is being awaited
  3. ask whether that object eventually reaches non-blocking external readiness
  4. ask what happens if it fails, stalls, or never yields

For production lifecycle, cancellation, and bounded fan-out, see .

Further depth
  • Language reference: await expression
  • Language reference: coroutine function definition
  • asyncio
  • Coroutines and Tasks
  • PEP 492: Coroutines with async and await syntax
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Every blocking boundary is everyone's problem.

RELATED GUIDES
NEXT CHECKS
Contribute