Eager tasks and performance

asyncio · Expert · 12 min

At high request rates, scheduling overhead becomes measurable and these tools reclaim it.

Every create_task normally schedules a first step on the next loop iteration. When many coroutines finish without ever suspending (a cache hit, for example), that's pure overhead.

Eager task execution (Python 3.12+) starts a task's coroutine immediately inside create_task. If it completes without suspending, the task is returned already done and never touches the loop's scheduler. Enable it for a loop with loop.set_task_factory(asyncio.eager_task_factory). Semantics change slightly: code runs before create_task returns, so ordering-sensitive code can behave differently. Test before switching.

Other levers:

  • Await coroutines directly instead of wrapping them in tasks when you don't need concurrency.
  • Batch work: one task processing 100 items beats 100 tasks.
  • Avoid wait_for in hot loops; asyncio.timeout is cheaper.
  • uvloop, a third-party drop-in loop built on libuv, often makes network-heavy servers 2-4x faster (uvloop.run(main())).
  • Profile with cProfile or py-spy; asyncio overhead often hides in task creation and callback scheduling.

Most asyncio slowness is not the loop at all but blocking calls, so check debug mode first.

Example

import asyncio, time

CACHE = {i: i * i for i in range(10_000)}

async def lookup(k):
    if k in CACHE:                       # completes without suspending
        return CACHE[k]
    await asyncio.sleep(0)

async def run(eager):
    loop = asyncio.get_running_loop()
    if eager:
        loop.set_task_factory(asyncio.eager_task_factory)
    t = time.perf_counter()
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(lookup(i)) for i in range(10_000)]
    print(f"eager={eager}: {time.perf_counter() - t:.3f}s")

asyncio.run(run(False))   # e.g. eager=False: 0.045s
asyncio.run(run(True))    # e.g. eager=True: 0.015s

Remember

  • eager_task_factory runs a task's first step synchronously inside create_task
  • Tasks that finish without suspending skip scheduling entirely
  • Batching, fewer tasks and uvloop are the other big wins

Watch out for

  • Switching on eager tasks without testing; code that relied on tasks starting later can change order.

Check yourself

  1. With eager_task_factory, what happens to a task whose coroutine returns without any suspending await?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/asyncio-task.html#eager-task-factory

Builds on

Unlocks

Nothing builds on this yet.