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_forin hot loops;asyncio.timeoutis cheaper. - uvloop, a third-party drop-in loop built on libuv, often makes network-heavy servers 2-4x faster (
uvloop.run(main())). - Profile with
cProfileor 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.015sRemember
- 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.