Skip to main content
  1. Posts/

Async Python Is Secretly Deterministic

··250 words·2 mins·

🐍 The big secret of asyncio: tasks always start in the same order

When working with durable workflows in Python, you need them to be deterministic for replay-based recovery. But how do you achieve that when steps run concurrently with asyncio.gather?

🔑 The key is the event loop:

  • asyncio is single-threaded: it only runs one task at a time
  • When you call asyncio.gather(coro1, coro2, coro3), tasks are enqueued in FIFO order
  • Tasks always start in the same deterministic order, even if their completion is unpredictable
  • A task yields control to the event loop only when it awaits something that isn’t ready
# This start order is always deterministic:
results = await asyncio.gather(step1(), step2(), step3())
# step1 starts first, then step2, then step3

🏗️ Leveraging this for durable workflows: DBOS’s @Step() decorator assigns an ID before the first await. Since assignment happens in deterministic start order, all steps have consistent IDs across executions → seamless recovery.

✅ The single-threaded model is actually easier to reason about than parallel threads, because tasks can only interleave when they explicitly yield control with await.

💡 Explanation in a nutshell
#

Python async seems chaotic because multiple tasks run “at the same time.” But there’s a hidden rule: all tasks start in the order you create them. This lets fault-recovery systems replay exactly what happened and in what order, even if tasks finish at different times.

More information at the link 👇

Also published on LinkedIn.
Juan Pedro Bretti Mandarano
Author
Juan Pedro Bretti Mandarano