Scrapeless Wiki

What Is asyncio? – Definition and How It Works

Informational P2 what is asyncio

Learn what asyncio is, how the event loop and coroutines work, why it suits I/O-bound work, and the mistakes that silently destroy concurrency.

asyncio is Python's built-in framework for concurrent I/O. A single thread runs an event loop that switches between tasks whenever one is waiting, so thousands of network operations can be in flight at once.

1. The Model

  • Coroutines β€” functions defined with async def, which can suspend at await.
  • The event loop β€” runs coroutines, resuming each when what it waited for is ready.
  • Tasks β€” coroutines scheduled on the loop, running concurrently.
  • Awaitables β€” anything a coroutine can wait on.
import asyncio, aiohttp

async def fetch(session, url):
    async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
        response.raise_for_status()
        return await response.text()

async def main(urls):
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

2. Concurrency, Not Parallelism

One thread runs everything. When a coroutine awaits a network response, the loop runs another β€” so waiting overlaps, but computation does not.

That makes asyncio excellent for I/O-bound work and useless for CPU-bound work, where processes are the answer.

3. Why It Suits Network Work

Fetching a page is mostly waiting. With threads, each concurrent request costs a thread and its stack; with asyncio, each costs a small object.

The practical consequence is that thousands of concurrent connections are ordinary rather than exotic.

4. The Mistake That Kills It

A blocking call inside a coroutine stops the entire loop β€” every other task waits.

# blocks the loop; nothing else runs
data = requests.get(url).text

# correct: an async client
async with session.get(url) as response:
    data = await response.text()

Common offenders: synchronous HTTP libraries, blocking database drivers, time.sleep, and CPU-heavy work. The symptom is confusing β€” code that is async everywhere yet runs no faster than sequential.

5. Bounding Concurrency

gather over ten thousand URLs starts ten thousand requests at once, which exhausts file descriptors and overwhelms destinations.

semaphore = asyncio.Semaphore(20)

async def fetch_limited(session, url):
    async with semaphore:
        return await fetch(session, url)

A semaphore per host is the standard control, and politeness requires it regardless of what your machine can handle.

6. Handling Failures in a Group

gather fails the whole group on the first exception unless told otherwise. Passing return_exceptions=True collects failures alongside results so one bad URL does not discard the rest.

Task groups provide structured alternatives with clearer cancellation semantics.

7. Timeouts Are Mandatory

Without a timeout, one unresponsive host holds a task forever. Every network call in production should carry one, and the whole operation should carry an outer bound as well.

8. Summary

asyncio runs concurrent I/O on one thread through an event loop and coroutines, making thousands of simultaneous network operations practical.

Its rules are strict: no blocking calls inside coroutines, bounded concurrency per host, explicit timeouts, and deliberate handling of failures within a group. Break the first and the concurrency silently disappears.