[Python] Max throughput in Python WebRTC data channel capped at ~110 Mb/s for large file...

Discussão em 'Python' iniciado por Stack, Agosto 29, 2025.

  1. Stack

    Stack Membro Participativo

    I’m building a peer-to-peer file transfer tool in Python using aiortc and WebRTC data channels. It’s designed to handle large files efficiently by reading 64 MB blocks from disk and sending them in 256 KB chunks over a data channel. My setup:

    Disk read size: 64 MB

    Network chunk size: 256 KB

    Max buffered amount: 32 * 256 KB

    Using two data channels: one for file data, one for control messages.

    Using asyncio queues to pipeline chunks.

    Problem: Even when both peers run on the same machine (localhost), the maximum throughput I achieve is ~110 Mb/s. The bufferedAmount in the data channel always seems to be at the MAX_BUFFERED_AMOUNT limit. The sending loop is constantly waiting for buffer space, which seems to throttle throughput.

    Core part of the app:

    class FileSender:
    def __init__(self, file_path):
    self._file_path = file_path
    self._file_channel = None
    self._control_channel = None
    self._done = asyncio.Future()
    self._buffer_event = asyncio.Event()
    self._buffer_event.set()
    self._chunk_queue = asyncio.Queue(maxsize=128) # enough to pipeline many chunks

    def set_channel(self, channel_type, channel):
    if channel_type == "file":
    self._file_channel = channel
    self._file_channel.on("open", self._on_both_channels_open)
    self._file_channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT
    self._file_channel.on("bufferedamountlow", self._on_buffered_amount_low)
    elif channel_type == "control":
    self._control_channel = channel
    self._control_channel.on("open", self._on_both_channels_open)
    self._control_channel.on("message", lambda message: asyncio.create_task(self._on_control_message(message)))

    def _on_both_channels_open(self):
    if self._file_channel.readyState == "open" and self._control_channel.readyState == "open":
    asyncio.create_task(self._start_file_transfer())

    def _on_buffered_amount_low(self):
    self._buffer_event.set()

    async def wait_until_done(self):
    await self._done

    async def _file_reader(self):
    """Read large blocks from disk and push slices into the queue."""
    async with aiofiles.open(self._file_path, "rb") as f:
    while True:
    block = await f.read(DISK_READ_SIZE)
    if not block:
    break
    # Slice into 256 KB chunks but keep them in memory until queued
    for i in range(0, len(block), NETWORK_CHUNK_SIZE):
    await self._chunk_queue.put(block[i:i + NETWORK_CHUNK_SIZE])
    await self._chunk_queue.put(None) # EOF

    async def _start_file_transfer(self):
    metadata = self._construct_metadata(self._file_path)
    self._control_channel.send(ControlMessage.create_json("metadata", json.dumps(metadata)))
    progress = Progress(metadata["file_size"])

    reader_task = asyncio.create_task(self._file_reader())

    while True:
    chunk = await self._chunk_queue.get()
    if chunk is None:
    break

    # Wait for channel buffer only if needed
    while self._file_channel.bufferedAmount > MAX_BUFFERED_AMOUNT:
    self._buffer_event.clear()
    await self._buffer_event.wait()

    self._file_channel.send(chunk)
    progress.update(chunk)

    await reader_task
    self._control_channel.send(ControlMessage.create_json("eof", metadata["file_name"]))


    class FileReceiver:
    def __init__(self, path):
    self._file_channel = None
    self._control_channel = None
    self._metadata = None
    self._location = None
    self._progress = None
    self._path = path
    self._done = asyncio.Future()
    self._chunk_queue = asyncio.Queue(maxsize=256) # pipeline more chunks
    self._writer_task = None

    async def wait_until_done(self):
    await self._done

    def set_channel(self, channel_type, channel):
    if channel_type == "file":
    self._file_channel = channel
    self._file_channel.on("message", lambda msg: self._chunk_queue.put_nowait(msg))
    elif channel_type == "control":
    self._control_channel = channel
    self._control_channel.on("message", lambda msg: asyncio.create_task(self._on_control_message(msg)))

    async def _process_file(self):
    self._location = os.path.join(self._path, self._metadata["file_name"])
    self._progress = Progress(self._metadata["file_size"])

    async with aiofiles.open(self._location, "wb") as f:
    while True:
    chunk = await self._chunk_queue.get()
    if chunk is None:
    break
    await f.write(chunk)
    self._progress.update(chunk)

    # checksum validation
    if compute_hash(self._location) != self._metadata["hash"]:
    print("[ERROR] File corrupted")
    else:
    print("File received successfully")

    self._control_channel.send(ControlMessage.create_json("transfer_complete", self._metadata["file_name"]))
    self._done.set_result(None)
    self._file_channel.close()
    self._control_channel.close()

    async def _on_control_message(self, message):
    control_message = ControlMessage.from_json(message)
    if control_message.msg_type == "metadata":
    self._metadata = control_message.data
    print(f"Receiving file: {self._metadata['file_name']} ({self._metadata['file_size']} bytes)")
    self._writer_task = asyncio.create_task(self._process_file())
    elif control_message.msg_type == "eof":
    await self._chunk_queue.put(None)


    Questions:

    Is the throughput limitation inherent to a single Python WebRTC data channel?

    Would creating multiple parallel data channels significantly improve throughput?

    Are there Python-side optimizations I’m missing, or is this a case where a lower-level language like C# or C++ would be needed?

    Should I just switch to a different protocol for higher throughput instead of using WebRTC data channels? If yes, do you have any recommendations? (It has to be UDP based to be able to hole-punch.)

    I’m looking for guidance on maximizing WebRTC data channel throughput in Python for large file transfers, and whether another protocol might be a better fit.

    Continue reading...

Compartilhe esta Página