Eager tasks nearly halve the cost of a cache hit
Python 3.12 added an eager task factory: a new task starts running inside create_task instead of waiting for the next loop turn, and if it finishes without ever suspending it never touches the scheduler. I measured what that is worth with gather over many small coroutines. When the coroutine returns without waiting (a cache hit), eager tasks cost about half as much per task. When it has to wait once, they save about 10%.
The test
CPython 3.14.8, one process per run. gather N coroutines and report ns per task, median of 9 gathers, three runs per cell. "Hit" is a coroutine that does a dict lookup and returns. "Miss" does await asyncio.sleep(0) first, so it suspends once. Runs were serial and the three repeats agree within about 5%.
| coroutine | N | default | eager | ratio |
|---|---|---|---|---|
| hit | 100 | 1,040 | 542 | 0.52 |
| hit | 10,000 | 1,214 | 647 | 0.53 |
| miss | 100 | 1,394 | 1,272 | 0.91 |
| miss | 10,000 | 1,670 | 1,484 | 0.89 |
Why
A default task costs a Task object, a scheduled callback, a loop turn and a done-callback wake-up for gather. An eager task that finishes immediately skips the schedule and the loop turn; only the object and the result are left. A task that suspends still pays for most of it, so the saving shrinks to roughly the one initial loop turn.
The catch
Order changes. With the default factory the new coroutine's first line runs after the caller's next await; with the eager one it runs before create_task returns:
| factory | order of events |
|---|---|
| default | after create, task |
| eager | task, after create |
Any code that sets something up after create_task and expects the task to see it (a registered handle, a lock, a name) can break quietly.
So what
If you fan out lots of calls that are often answered from a cache, set loop.set_task_factory(asyncio.eager_task_factory) and expect about half the overhead on the hits. If everything awaits real I/O, the gain is under 10% and not worth the ordering risk. Test with your own hit rate: the saving scales with the share of tasks that finish without suspending.
Method: one machine, a 25-line script, near-empty coroutines. Real handlers do more work per task, so the absolute saving matters most when the handler is nearly free.