Skip to content

asyncio: run newly scheduled callbacks eagerly before polling for I/O #160760

Open

Description

@asvetlov

Feature or enhancement

Currently, BaseEventLoop._run_once() runs only the callbacks that were ready when the iteration started. Callbacks scheduled by those callbacks wait for the next iteration, which first polls the selector (select()/epoll_wait()) with a zero timeout. Code that resolves futures, wakes tasks or chains call_soon() therefore pays for one I/O poll per hop, even when there is no I/O to process.

The good example is using asyncio.Queue: await q.put(item) pushes an item into the queue and wakes up the getter on the next event loop iteration only.
The same is true with network protocols: StreamReaderProtocol.data.received() feeds data to StreamReader and wakes await reader.read() after selector.select() call. Custom code that works with asyncio protocols (or loop.sock_recv()) explicitly suffers from the same problem.
If the code uses more moving parts, e.g. protocol->message-parser->user-code, each indirection level adds its own delayed call that increases latency.

I propose letting the loop keep running newly scheduled callbacks for a short, bounded time before it polls for I/O again (“eager processing”).

If after running scheduled callbacks (asyncio.Handler instances) there are new callbacks registered for the next loop iteration, they are executed immediately.

The implementation adds time- and count-based limits for eager callback execution for preventing possible infinite callbacks chain.

Has this already been discussed elsewhere?

I have already discussed this feature proposal on Discourse

Links to previous discussion of this feature:

https://discuss.python.org/t/an-eager-way-to-set-result-on-asyncio-future/106161/27

Linked PRs

Activity

  1. YvesDup commented on Oct 27, 2026

    @YvesDup
    Contributor

    The good example is using asyncio.Queue: await q.put(item) pushes an item into the queue and wakes up the getter on the next event loop iteration only.

    If I clearly understand your description, does it cover and fix the following example, that breaks the FIFO's order of a queue ?

    import asyncio
    
    async def simple_exemple():
       n = 0
       q = asyncio.Queue(1)
    
       asyncio.create_task(q.put(n:=n+1))
       print(f"put: {n}")
       asyncio.create_task(q.put(n:=n+1))
       print(f"put: {n}")
       await asyncio.sleep(0.0)
    
       print(f"\tget: {await q.get()}")
    
       q.put_nowait(n:=n+1)
       print(f"put: {n}")
    
       print(f"\tget: {await q.get()}")
       print(f"\tget: {await q.get()}")
    
    asyncio.run(simple_exemple())

    The output result is:

        put: 1
        put: 2
           get: 1
        put: 3
           get: 3
           get: 2
  2. added 6 commits that reference this issue on Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions