Skip to Content
Course content

209: The Event Loop Explained

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've started playing with async and await, you've probably noticed that it feels a bit like magic. You mark a function as async, you await some call, and suddenly your code is doing five things at once. But if you don't understand the event loop, you're essentially driving a car without knowing how the engine works—it's fine until you hit a weird bug, and then you're completely stranded.

Let's look at a scenario I deal with all the time: fetching data from a handful of external APIs. Suppose we need to check the current price of five different cryptocurrencies from a public API. The naive way to do this is the way most of us learned first—synchronous, sequential execution.

import requests
import time

def get_prices():
    coins = ['bitcoin', 'ethereum', 'solana', 'cardano', 'polkadot']
    for coin in coins:
        # This blocks the entire program until the server responds
        response = requests.get(f"https://api.coinbase.com/v2/prices/{coin}/spot")
        print(f"Fetched {coin}")

start = time.perf_counter()
get_prices()
print(f"Total time: {time.perf_counter() - start:.2f} seconds")

The cost of idling

In the code above, your CPU is doing almost nothing. When requests.get() is called, the Python process literally stops. It sits there, staring at the network socket, waiting for a packet to come back from the server. If the server takes 500ms to respond, your program is frozen for 500ms. Multiply that by five coins, and you've wasted two and a half seconds just waiting. In a production environment with thousands of requests, this is where your application crawls to a halt.

Now, you might think, "Why not just use threads?" You could, but threads are heavy. They consume significant memory and the OS has to spend a lot of effort swapping between them. This is where the event loop comes in. Instead of multiple threads, we use one single thread and a very clever "to-do list."

The event loop is essentially a while True loop that keeps track of all your running tasks. When a task hits an await expression, it's telling the loop: "I'm going to be waiting for the network for a while. Feel free to run other tasks; just wake me up when the data arrives."

import asyncio
import aiohttp
import time

async def fetch_price(session, coin):
    # Instead of blocking, we 'await' the response
    async with session.get(f"https://api.coinbase.com/v2/prices/{coin}/spot") as response:
        await response.json()
        print(f"Fetched {coin}")

async def main():
    coins = ['bitcoin', 'ethereum', 'solana', 'cardano', 'polkadot']
    async with aiohttp.ClientSession() as session:
        # Schedule all calls to run concurrently
        tasks = [fetch_price(session, coin) for coin in coins]
        await asyncio.gather(*tasks)

start = time.perf_counter()
asyncio.run(main())
print(f"Total time: {time.perf_counter() - start:.2f} seconds")

Trading sequentiality for concurrency

Notice what happened here. We didn't create five threads. We created five coroutines. When the first fetch_price call hits await session.get(), it pauses and hands control back to the event loop. The loop looks at its list, sees that the second fetch_price is ready to start, and jumps into that one. It keeps bouncing between these tasks, initiating the requests almost simultaneously.

The trade-off here is that you have to be disciplined. If you accidentally put a "blocking" call (like time.sleep() or requests.get()) inside an async function, you've just broken the loop. Because there is only one thread, a blocking call doesn't just pause that one task—it freezes the entire event loop. Every other pending task will just sit there, waiting for that one blocking call to finish. I've seen entire production servers go offline because someone put a synchronous database driver inside an async route.

Think of the event loop as a single waiter in a restaurant. A bad waiter (synchronous) takes an order, walks to the kitchen, and stands there staring at the chef until the food is ready before bringing it to the table. A great waiter (event loop) takes the order, hands the ticket to the kitchen, and immediately goes to take orders from three other tables while the food is cooking. The waiter isn't cooking the food faster; they're just managing their downtime more efficiently.




📋 Practical Task

Building a Concurrent Site-Availability Checker

Your task is to create a script that checks the HTTP status of a list of websites to see if they are online. You must use asyncio and aiohttp to ensure the checks happen concurrently rather than one after another.

  • Create a list of at least 10 different URLs (mix in some that you know might be slow or invalid).
  • Write an async function called check_status(session, url) that fetches the URL and returns a string stating whether the site is "UP" (status 200) or "DOWN" (anything else/exception).
  • Write a main() function that uses asyncio.gather() to run all these checks concurrently.
  • Wrap your network call in a try/except block to handle aiohttp.ClientError so a single dead link doesn't crash your entire loop.
  • Print the total time elapsed at the end to prove the concurrency is working.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.