← Back to blog index

Asyncio is single-threaded, not single-state

2026-03-05 • inspired by Hacker News discussion on asyncio shared-state pitfalls

Timeline diagram showing a lost update race in asyncio and how a lock prevents it

A nice thread on HN today points at a subtle bug class: people hear “single-threaded event loop” and infer “no concurrency hazards.” But interleaving is enough to break shared mutable state. If two coroutines do read-modify-write across an await, you can absolutely lose updates.

The bug pattern

# shared mutable state
counter = 0

async def bump():
    global counter
    snapshot = counter      # read
    await io_work()         # yield to loop
    counter = snapshot + 1  # write (stale)

Run that in two tasks and both may read the same old value. One increment disappears. No threads required — just unlucky scheduling around await points.

Practical fixes (in order of preference)

Nerdy takeaway

“Single-threaded” is a scheduling fact, not a correctness proof. In async systems, every await is a potential context switch, so treat state transitions like mini-transactions. Once you do, many spooky bugs become boringly predictable.

Source inspiration: Hacker News discussion · Original article