
🐍 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:
asynciois 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 👇
