python/cpython · error · RuntimeError

Empty waiter is already set

Error message

Empty waiter is already set

What it means

Raised by _SelectorSocketTransport._make_empty_waiter() as RuntimeError when an empty-waiter future already exists. The empty waiter is an internal future created at the start of a sendfile operation to wait for the write buffer to drain; a second one is only possible if sendfile is initiated while a previous sendfile is still pending, which the transport forbids.

Source

Thrown at Lib/asyncio/selector_events.py:1223

        if self._buffer:
            self._add_writer(self._sock_fd, self._write_ready)
            self._maybe_pause_protocol()

    def can_write_eof(self):
        return True

    def _call_connection_lost(self, exc):
        try:
            super()._call_connection_lost(exc)
        finally:
            self._write_ready = None
            if self._empty_waiter is not None:
                self._empty_waiter.set_exception(
                    ConnectionError("Connection is closed by peer"))

    def _make_empty_waiter(self):
        if self._empty_waiter is not None:
            raise RuntimeError("Empty waiter is already set")
        self._empty_waiter = self._loop.create_future()
        if not self._buffer:
            self._empty_waiter.set_result(None)
        return self._empty_waiter

    def _reset_empty_waiter(self):
        self._empty_waiter = None

    def close(self):
        self._read_ready_cb = None
        super().close()


class _SelectorDatagramTransport(_SelectorTransport, transports.DatagramTransport):

    _header_size = 8

    def __init__(self, loop, sock, protocol, address=None,

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Serialize sendfile calls per transport: await each loop.sendfile() before starting the next.
  2. Protect the transport with an asyncio.Lock or a per-connection worker queue.
  3. Await/cancel outstanding sendfile tasks before starting a new one or closing the connection.

Example fix

// before
asyncio.create_task(loop.sendfile(transport, f1))
await loop.sendfile(transport, f2)  # RuntimeError: Empty waiter is already set

// after
async with conn_lock:
    await loop.sendfile(transport, f1)
    await loop.sendfile(transport, f2)
Defensive patterns

Strategy: validation

Validate before calling

file_lock = asyncio.Lock()

async def send_one_at_a_time(transport, paths):
    for p in paths:
        async with file_lock:  # prevents overlapping sendfiles
            with open(p, 'rb') as f:
                await loop.sendfile(transport, f)

Try / catch

try:
    await loop.sendfile(transport, f)
except RuntimeError as e:
    if 'Empty waiter' in str(e):
        await prior_sendfile_task  # drain previous sendfile, then retry
        await loop.sendfile(transport, f)
    else:
        raise

Prevention

When it happens

Trigger: Two overlapping sendfile operations on the same transport: e.g. calling loop.sendfile(transport, f1) and, before it resolves, loop.sendfile(transport, f2); typically via concurrent tasks sharing one StreamWriter.

Common situations: A file-server handler processing pipelined requests concurrently over a single connection; retry logic that re-issues sendfile without awaiting the first; fire-and-forget task groups writing files to the same client connection.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/8740cb332bcf5ef8. Report an issue: GitHub.