Skip to content
← Back to explainers

Python explainer

Start Tasks Right Away with Eager Tasks

About 4 min to read. Allow extra time to work through examples.

Let a task run inside create_task until its first real wait, so cache hits finish without waiting for the event loop, and see what that changes for ordering and cancellation.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your service looks up users through a cache, and nine lookups in ten are cache hits. Each lookup runs in its own task. An ordinary task does not start when you create it. It waits for the event loop's next turn. For a cache hit that never waits for anything, that delay is pure overhead.

An eager task skips the delay. It starts running its coroutine inside create_task itself, and keeps going until it reaches an await that has to wait, or until it finishes.

import asyncio


async def main(eager: bool) -> None:
    if eager:
        asyncio.get_running_loop().set_task_factory(asyncio.eager_task_factory)
    log: list[str] = []

    async def worker() -> None:
        log.append("worker")

    log.append("before")
    task = asyncio.create_task(worker())
    log.append("after")
    await task
    print(log)


asyncio.run(main(eager=False))  # ['before', 'after', 'worker']
asyncio.run(main(eager=True))   # ['before', 'worker', 'after']

With an ordinary task, create_task only schedules worker, so "after" comes first. With an eager task, worker runs to the end before create_task returns.

Turn on eager tasks for the whole loop

Python 3.12 added asyncio.eager_task_factory. Install it with loop.set_task_factory once, at the top of your main coroutine. From then on, every task on that loop is eager, including the tasks your libraries create. Each asyncio.run call makes a fresh loop, so the example can try both settings.

You still get an ordinary Task back. Await it, or create it through a TaskGroup, as before. If the coroutine raises, the task stores the exception and await task raises it, the same as with an ordinary task.

The task runs until its first real wait

An await pauses a task only when the result is not ready yet. Awaiting a coroutine that returns without waiting does not pause the task, and neither does awaiting a future that is already done. So an eager task can pass several await lines before one of them pauses it. A cache hit may never pause at all, and then the whole task finishes inside create_task.

Common mistakes

Three habits that worked with ordinary tasks cause bugs with eager ones.

  • Setting up shared state after creating the task. The task may already have read it. Prepare everything the task needs before you call create_task.
  • Cancelling a task right after creating it. By the time you call task.cancel(), the eager part has already run. If the task has finished, cancel() returns False and its effects stay.
  • Slow synchronous work before the first await. That work now runs inside create_task, so the caller waits for it. Eager tasks run your code sooner, on the same thread. The work still blocks the event loop.
Check your understanding You hold an asyncio.Lock and create an eager task that needs the same lock. Does the program deadlock? Show answer

No. The task runs until async with lock finds the lock taken, then pauses there, and create_task returns. Your code carries on and releases the lock, and the task gets it next.

You would deadlock by awaiting the task while you still hold the lock. That deadlocks with ordinary tasks too.

Measure before you switch

Eager tasks save time when many tasks finish without waiting, such as cache hits or results that are already computed. Benchmark with realistic hit and miss rates, and compare throughput and latency before and after the change.

If a test that checks ordering fails after the switch, your code relied on the old order. Make that order explicit, for example with an asyncio.Event, or keep ordinary tasks on that loop.

Going further

Python 3.14 lets you make a single task eager without changing the loop. Pass eager_start=True to asyncio.create_task or TaskGroup.create_task. Python 3.12 and 3.13 reject that keyword with a TypeError. If you use a custom event loop or task factory, check that it supports eager start.

Read more in the asyncio documentation on eager tasks.

step 47 of 56

Concurrency with Threads and asyncio

Up next Limit Concurrency, Request Rate and Queued Work