惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tailwind CSS Blog
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
GbyAI
GbyAI
爱范儿
爱范儿
量子位
Martin Fowler
Martin Fowler
V
V2EX
博客园 - 三生石上(FineUI控件)
I
InfoQ
MongoDB | Blog
MongoDB | Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
Netflix TechBlog - Medium
D
DataBreaches.Net
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Last Week in AI
Last Week in AI
U
Unit 42
Apple Machine Learning Research
Apple Machine Learning Research
H
Help Net Security
T
The Blog of Author Tim Ferriss
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
Engineering at Meta
Engineering at Meta

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
asyncio Pitfalls: The 3-Hour Bug
BAOFUFAN · 2026-05-01 · via DEV Community

BAOFUFAN

Last week, my boss asked me to speed up an old web scraping project. I thought, "No problem — I’ll just throw asyncio at it, fetch concurrently, and theoretically slash 200 requests from 40 seconds to around 2." But after writing the first version, while it was indeed faster, half the data was missing and the console was flooded with RuntimeWarning: coroutine was never awaited. Even worse, the program would randomly freeze with 0% CPU and eventually time out. After three hours of poring over docs and source code, I finally filled in all the pitfalls. This post captures those gotchas and the best practices I ended up with, so you don’t have to go down the same rabbit hole.


The Scenario: 200 API data fetches, from 40s to 2s

The original synchronous code looked like this — simple but painfully slow:

import time
import requests

def fetch_all(urls):
    results = []
    for url in urls:
        resp = requests.get(url, timeout=5)
        results.append(resp.json())
    return results

urls = [f"https://api.example.com/item/{i}" for i in range(200)]
start = time.time()
data = fetch_all(urls)
print(f"耗时: {time.time() - start:.2f}s")
# 输出: 耗时: 41.23s

Enter fullscreen mode Exit fullscreen mode

200 sequential requests taking over 40 seconds — a terrible experience. Brimming with confidence, I set out to rewrite it with asyncio.


Core Concept: Event Loop + Coroutines = Non-blocking Concurrency

asyncio works completely differently from multithreading. It runs a single-threaded event loop where all coroutines are scheduled within the same thread. When a coroutine hits an IO wait (network, disk), it voluntarily yields control back to the event loop via await. The loop immediately switches to other coroutines that are ready to run. This way, the CPU never idly spins waiting for IO.

The most basic pattern:

import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        return await resp.json()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    return results

urls = [f"https://api.example.com/item/{i}" for i in range(200)]
asyncio.run(main())

Enter fullscreen mode Exit fullscreen mode

asyncio.gather schedules all coroutines concurrently, so the total time is roughly the duration of the slowest request, not the sum. In theory, wonderful — but as soon as I ran it, the pitfalls began.


Pitfall 1: Sneaky Synchronous Blocking Calls Inside Coroutines

At first, I took a shortcut and kept using requests.get inside the coroutine, thinking simply wrapping it in async def would work. But the event loop got completely stuck on requests.get — concurrency went out the window.

# 错误示范
import requests

async def fetch_bad(url):
    resp = requests.get(url)   # 同步阻塞!事件循环被堵死
    return resp.json()

Enter fullscreen mode Exit fullscreen mode

The requests library does synchronous I/O. Once called, the entire thread blocks waiting for the network response, and the event loop loses all control. For asyncio, you must use libraries that support async/await — like aiohttp for HTTP and aiomysql for database queries.

Solution: Replace all I/O with libraries from the async/await ecosystem. For unavoidable synchronous code, offload it to a thread pool using loop.run_in_executor:

import concurrent.futures

def sync_heavy_work(data):
    # 这是一个没法改写的同步 CPU 计算
    return sum(i * i for i in range(data))

async def run_in_thread(data):
    loop = asyncio.get_running_loop()
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, sync_heavy_work, data)
    return result

Enter fullscreen mode Exit fullscreen mode

This prevents blocking the event loop, but don't overuse it — thread switching overhead still exists.


Pitfall 2: gather Raises on First Exception, Discarding Other Results

With 200 requests, a couple of timeouts or 500s are expected. The first time I used gather, if a single task raised an exception, the entire gather would immediately throw, and the other 190+ successful responses were discarded. I was so frustrated I slapped my thigh.

results = await asyncio.gather(*tasks)  # 一个挂了,全部白干

Enter fullscreen mode Exit fullscreen mode

Solution: gather has a return_exceptions=True parameter. Instead of raising, it places exception objects directly into the result list, so you can handle them like regular return values:

results = await asyncio.gather(*tasks, return_exceptions=True)

for i, res in enumerate(results):
    if isinstance(res, Exception):
        print(f"任务 {i} 失败: {res}")
    else:
        process(res)

Enter fullscreen mode Exit fullscreen mode

This way, even if 10 tasks timeout, you still get the data from the remaining 190. I also like to add timeout and retry logic inside fetch:

from aiohttp import ClientTimeout

async def fetch(session, url, retries=2):
    for attempt in range(retries):
        try:
            async with session.get(url, timeout=Clien

Enter fullscreen mode Exit fullscreen mode