Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Blocking vs. Non-Blocking I/O: Performance in Web Applications

Recommended resources

Welcome to the fourth module of our course! In our previous discussions, we designed the high-level architecture of our microservices, focusing on service boundaries and data management. We also uncovered critical anti-patterns like the "distributed monolith," where synchronous, blocking communication creates a fragile and slow system. This highlighted a clear need for a different approach to building resilient, high-performance services.

This module marks our shift from architectural theory to the practical coding skills required to implement these modern systems. We will take a deep dive into Async Python, the foundation for building fast, non-blocking applications. Today's lesson tackles the most fundamental concept: the difference between blocking and non-blocking I/O, and why it is a game-changer in a web context.

What is I/O and Why Does It Dominate Backend Systems?

In computing, I/O stands for Input/Output. It refers to any operation where your program communicates with the "outside world"—anything outside the CPU. For a backend developer, common I/O operations include:

  • Making a network request to another service (like an API call).
  • Querying a database.
  • Reading a file from a disk.
  • Writing logs.

These tasks are called I/O-bound because the total execution time is dominated by waiting for the I/O operation to complete, not by CPU calculations. In contrast, a CPU-bound task is one that spends most of its time doing intensive calculations, like processing a large dataset or rendering a complex image. Most microservices are overwhelmingly I/O-bound.

The video below introduces these concepts using a helpful restaurant analogy, which perfectly illustrates how a single worker (a thread) can handle requests.

Non-blocking I/O and how Node uses it, in friendly terms: blocking vs async IO, CPU vs IO

Watch this video, "Non-blocking I/O and how Node uses it," by Studying With Alex. Although it mentions Node.js, the core concepts are universal and apply directly to Async Python.

First, get an overview of how servers and threads work. Next, pay close attention to the restaurant analogy. This is key to building an intuition for non-blocking behavior. Finally, watch the explanation of CPU vs. I/O work to solidify your understanding of these terms.

The key insight from the video is that a waiter doesn't just stand still while a customer reads the menu. They use that "wait time" to serve other tables. This is the essence of non-blocking I/O.


Blocking vs. Non-Blocking I/O in Code

Let's translate this analogy into Python code. The difference between a blocking and non-blocking call is how it behaves during that "wait time."

  • Blocking I/O: The function call blocks the execution of the program. Your code literally pauses and waits for the I/O operation to finish before it can do anything else. It's like a waiter who pulls up a chair and stares at you while you read the menu, refusing to help anyone else. The standard time.sleep() is a classic example of a blocking call.

  • Non-Blocking I/O: The function call initiates the I/O operation and immediately returns control to your program. Your program is free to do other work. When the I/O operation is complete, the program is notified. This is like the efficient waiter who takes your order and then serves other tables, returning only when your food is ready. asyncio.sleep() is the non-blocking equivalent of time.sleep().

The following video provides a practical demonstration of this difference within a FastAPI application, an environment you're already familiar with.

How FastAPI Handles Requests Behind the Scenes

This video, "How FastAPI Handles Requests Behind the Scenes" by Code Collider, gives a concise and clear demonstration.

Watch the blocking example using time.sleep(). Notice how the server handles two requests one after the other. Then, see the non-blocking example with asyncio.sleep(). Observe how the "hello" from both requests is printed before the "bye". Understand the core difference between time.sleep and asyncio.sleep. Finally, see the explanation of how the event loop handles these requests. This explains why the behavior is different.

After watching, you should see that with blocking I/O, the total time to handle two 5-second requests is over 10 seconds. With non-blocking I/O, it's just over 5 seconds. The server works on the second request while the first one is "sleeping."


Demonstrating the Performance Impact

This performance difference isn't just a trick with sleep. It has a massive impact on real-world, I/O-bound tasks like network requests.

Let's study a clear example that compares a synchronous (blocking) script with an asynchronous (non-blocking) one.

Python's asyncio: A Hands-On Walkthrough

This article from Real Python provides excellent code examples that make the performance difference tangible.

Read the section titled "Async I/O in Python With asyncio". Focus on the countsync.py and countasync.py code snippets and their execution results. Notice how the total time drops from 6 seconds to 2 seconds. The explanation following the code clearly states why this happens: await asyncio.sleep() allows the program to run other tasks in the meantime.

The countasync.py example runs three tasks concurrently. Each task takes 2 seconds (two 1-second sleeps), but because they run in an overlapping manner, the entire program finishes in just over 2 seconds.

This principle is powerfully illustrated by the analogy of chess master Judit Polgár.

Python's asyncio: A Hands-On Walkthrough

Now, read the short section titled "Async I/O Explained" from the same Real Python article.

Focus on the Judit Polgár chess exhibition analogy. Think about how this maps to an API gateway needing to fetch data from multiple microservices to fulfill a single client request.

Playing asynchronously, Judit Polgár reduces a 12-hour exhibition to just 1 hour. This is the same dramatic performance improvement we gain in our software. When an API gateway needs to call the User, Order, and Product services, it doesn't have to wait for each one to finish sequentially. It can fire off all three requests at once and process them as they return.

Sequential vs. Concurrent I/O Performance with asyncio.gather
This diagram shows the performance benefit of non-blocking I/O. On the left, four I/O tasks run sequentially, and the total time is the sum of their individual times (530ms). On the right, using non-blocking I/O with `asyncio.gather`, the same tasks run concurrently. The total time is determined by the longest-running task (200ms).

A Real-World Benchmark

The sleep examples and analogies are great for understanding the concept, but let's look at a benchmark with actual HTTP requests, which is the daily bread of microservices communication.

The article below benchmarks different strategies for making a large number of HTTP requests. We will focus on two extremes: fully synchronous vs. fully asynchronous.

A Deep Dive into High Performance HTTP Requests for Python Engineers

This article from Klaviyo's engineering blog provides concrete performance data.

First, read the section "Synchronous HTTP client with a single thread". Note the code using the standard requests library and the result: 100 requests took about 14 seconds. Next, skip down to the section "Asynchronous HTTP client with asyncio". Observe the code using aiohttp (an async HTTP client) and the result from the chart: 1000 requests took about 2.5 seconds. Focus on the stark difference in performance. The synchronous method is slow and scales linearly, while the asynchronous method handles 10x the requests in a fraction of the time.

The results are staggering. The asynchronous approach isn't just a little faster; it's orders of magnitude more efficient. This is the performance impact that enables services to handle thousands of requests per second, which would be impossible with a purely blocking model on the same hardware.


Conclusion

In this lesson, we established the fundamental difference between blocking and non-blocking I/O. This isn't just an academic distinction; it is the core principle that enables high-performance, scalable network applications and microservices.

Key Takeaways:

  • I/O-bound tasks, like network calls and database queries, are dominated by waiting.
  • Blocking I/O wastes this waiting time, forcing a program to idle and handle requests sequentially. This leads to high latency and low throughput.
  • Non-blocking I/O utilizes this waiting time by allowing the program to work on other tasks. This enables concurrency, even on a single thread.
  • In a web context, this translates to drastically lower response times and the ability to handle a much higher volume of concurrent users.

Preview of the Next Lesson:

We've used the term "concurrency" to describe how async I/O allows tasks to run in an overlapping manner. You might also have heard the term "parallelism." Are they the same? In our next lesson, we will precisely define concurrency vs. parallelism, explain how they relate to I/O-bound and CPU-bound tasks, and clarify where Python's asyncio fits into the picture. This will give you the complete mental model needed to choose the right tool for the right job.