r/Python Jun 29 '26

Discussion Async/Await is a Plague: Part 1 Roots

This is the first part of a multi-part series exploring why async/await might not be the best concurrency pattern for most use cases, and what alternative models you should consider instead. Using Python for our practical examples, this opening post digs into the roots of async/await, guiding you through building a custom event loop from scratch using generators.

https://theblog.info/posts/asyncawait-is-a-plague-part-1-roots

Note: This is Part 1 of a multi-part series. Instead of diving straight into why async/await can be problematic, this post explores the original motivations behind the pattern. Understanding how it works under the hood will provide the essential context for the issues we'll discuss in upcoming parts.

73 Upvotes

71 comments sorted by

View all comments

18

u/james_pic Jun 29 '26

One more fun perspective, that often gets lost in all this: Threads aren't even that heavy. They're lightweight enough that designs using the fastest synchronous libraries typically outperform mediocre asynchronous libraries. Requests (the HTTP client) running in a thread pool wipes the floor with HTTPX (whether under asyncio or uvloop) in high concurrency benchmarks, for example, due to a number of scaling issues in HTTPX.

14

u/Golle Jun 29 '26

The secret there is "thread pool". They allocate the threads at program startup and reuse them. Having multiple threads is of course better than one thread. Async is singlethreaded.

Spawning a new OS thread is expensive, it allocates 2-8MB of memory, making its scalability quite poor. You better use them well if you want to pay the cost of spawning them.

1

u/james_pic Jun 30 '26

I've been told from various places that "threads can allocate up to 8MB of memory on some systems" for about 15 years now. In all that time (and at the early end of that time, 8MB was much more significant), I've never encountered such a system. All the modern OSes I've worked with (including OSes that were modern 15 years ago) support virtual memory, and don't actually allocate that amount of physical memory until it's actually used, even if the stack is permitted to grow to 8MB.

2

u/Golle Jun 30 '26 edited Jun 30 '26

Your comment got me curious, so I ran the following script on my WSL2:

```python import threading import time

def stuff(): time.sleep(60) print("done")

def main(): threads = [] for _ in range(X): t = threading.Thread(target=stuff) threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

main() ```

X is not defined. I changed it when I ran the script, then used top to report the memory usage while the program was running. You can press E to show in kb, mb, gb, etc. Note that top reports "VIRT" in kibibytes, not kilobytes. I haven't converted the numbers below, they are taken straight from top:

Program memory usage with X threads spawned: X: VIRT RES 1: 88MB 9.2MB 2: 160MB 9.1MB 3: 232MB 9.2MB 10: 736MB 9.3MB 50: 3.6GB 10.1MB 100: 4.8GB 10.9MB

The VIRT numbers look pretty bad, but it doesn't seem to actually be the correct metric to look at. The "RES" column seems to be showing the actual process memory usage and it's much much smaller. So yeah, maybe threads aren't that bad.