aiopquic - Async QUIC + WebTransport (picoquic)
aiopquic is a Python/Cython binding to picoquic, providing high-performance QUIC transport and WebTransport for asyncio applications.
Overview
aiopquic exposes picoquic's QUIC implementation through a lock-free SPSC ring buffer architecture that bridges the picoquic network thread with Python's asyncio event loop. It provides an asyncio QUIC/HTTP3 transport API in the spirit of aioquic (and its fork qh3) — similar shapes for QuicConfiguration, QuicConnection, connect / serve, and event types — plus a native WebTransport client/server layered on picoquic's H3 + h3zero. Not a drop-in replacement: semantics differ around backpressure (send_stream_data raises BufferError on full per-stream ring) and flow-control sizing.
Architecture
- SPSC Ring Buffers -- Lock-free single producer/single consumer rings for event passing between threads, separate TX and RX rings per
TransportContext. - TX path -- Asyncio pushes into per-stream byte ring; picoquic pulls at wire rate via
prepare_to_send. - RX path -- picoquic pushes per-event
StreamChunks; ownership transfers at pop for 1-copy delivery. - Cross-platform wake fd -- Linux
eventfdfor efficient asyncioadd_reader()notification;pipe()self-pipe fallback on macOS / BSD. - Dedicated Network Thread -- picoquic runs in its own thread via
picoquic_start_network_thread(). One worker thread perTransportContext; multiple contexts share the asyncio event loop within a single Python process. - Cython Bridge -- Thin Cython layer over C callbacks, minimal overhead.
- WebTransport --
asyncio.webtransport.WebTransportSession(client + server) over picoquic'spicowt_*API and h3zero.
Features
- QUIC client and server:
connect,serve,QuicConnectionProtocol - WebTransport client and server:
connect_webtransport,serve_webtransport,WebTransportSession; subprotocol negotiation (wt_available_protocols/wt_supported_protocols); static files over HTTP/3 (serve_webtransport(web_folder=...)) - Raw QUIC and WebTransport on one UDP port, dispatched by ALPN:
serve_dispatch - Streams: FIN, reset, stop_sending, and RESET_STREAM_AT (
reset_stream(..., reliable_size=N)); all-or-nothing writes with drain events for backpressure - QUIC and WebTransport datagrams, TX + RX
- Stream and datagram priority on one scale:
set_stream_priority,set_datagram_priority - Congestion control by name:
bbr(v3, default),bbr1,cubic,dcubic,newreno,fast,prague,c4 - Address validation with Retry (
serve(..., retry=True)), QUIC v2 by compatible version negotiation (supported_versions), session resumption and 0-RTT (session_ticket_file) - TLS: certificate verification against certifi's roots or a private CA, post-quantum key exchange
(
KEX_X25519MLKEM768, OpenSSL 3.5), cipher suite selection, key update interval - Dual-stack IPv4/IPv6 servers, a bind host,
ip_familyto pin one family - Connection limits:
max_connections, andset_max_connectionsat runtime (new connections are refused with CONNECTION_REFUSED) - Path MTU discovery policy:
pmtud="basic" | "delayed" | "blocked" - Connection management: idle timeout, keep-alive, application close codes
- Connection migration (inherited from picoquic)
- Per-connection multiplexing on the server side via
QuicEngine - qlog (
qlog_dir), TLS keylog (NSS Key Log Format) for pcap decryption, and a version report (python -m aiopquic.versions)
Performance
Multi-Gbps sustained throughput over loopback on commodity hardware: 2+ Gbps through the full asyncio API at the QUIC default MTU (where the kernel's sendmsg syscall rate is the wall), 11+ Gbps protocol-only. Numbers, methodology, reproduction commands, build flags, and runtime tuning (jemalloc, GSO, wake thresholds) live in PERFORMANCE.md. The TX/RX dataflow and flow-control model — including the TX backpressure configuration parameters — are documented in DATAFLOW.md.
Installation
Wheels for cp312 / cp313 / cp314 on Linux (manylinux_2_34, glibc 2.34+) and macOS arm64 are published to PyPI:
uv pip install aiopquic # or: pip install aiopquic
For older Linux (glibc 2.28–2.33) install via sdist; build toolchain required.
From source
git clone https://github.com/gmarzot/aiopquic.git
cd aiopquic
./bootstrap_python.sh # creates .venv with uv-managed Python 3.14 (GIL build) and pins cython 3.2+
source .venv/bin/activate
./build.sh # reconcile submodules, build picotls/picoquic + drivers, relink + verify
uv pip install -e '.[dev]' # add dev/test extras — or: pip install -e '.[dev]'
Re-run ./build.sh after any git checkout or submodule bump — it is idempotent (a matching fingerprint skips the compile) and guarantees the imported aiopquic reflects the current source, host-tuned. ./build.sh --check is a read-only doctor that reports submodule drift, a stale native build, or a portable wheel shadowing the editable install, and exits nonzero — handy as a pre-benchmark or CI gate.
On macOS, set OPENSSL_ROOT_DIR if Homebrew OpenSSL is not auto-detected (the build script tries openssl@3 then openssl@1.1).
Reporting issues
Include the full version report in any issue — it captures aiopquic plus the picoquic + picotls submodule SHAs the binding was built from:
python -m aiopquic.versions # or the console script: aiopquic-versions
Sample output:
aiopquic: 0.7.0rc2 (.venv/lib/python3.13/site-packages/aiopquic) [2026-10-29 09:21]
- picoquic: 1.1.53.0 (2026.10.22.70b5fd5-0-70b5fd5a) [2026-10-22]
- picotls: master (f06553b8) [2026-09-30]
- openssl: OpenSSL 3.5.8 (.venv/lib/python3.13/site-packages/aiopquic.libs/libcrypto-c37e28d8.so.3.5.8)
If you're running aiomoqt on top, prefer python -m aiomoqt.versions — it chains through to this report and includes the aiomoqt version too.
Usage
QUIC client and server
Subclass QuicConnectionProtocol and override quic_event_received; self._quic is the
connection. An echo server:
import asyncio
from aiopquic.asyncio import QuicConnectionProtocol, serve
from aiopquic.quic.configuration import QuicConfiguration
from aiopquic.quic.events import StreamDataReceived
class Echo(QuicConnectionProtocol):
def quic_event_received(self, event):
if isinstance(event, StreamDataReceived):
self._quic.send_stream_data(event.stream_id, bytes(event.data),
end_stream=event.end_stream)
async def main():
cfg = QuicConfiguration(is_client=False, alpn_protocols=["echo"])
cfg.load_cert_chain("cert.pem", "key.pem")
server = await serve("::", 4433, configuration=cfg, create_protocol=Echo)
try:
await asyncio.Future() # serve until cancelled
finally:
server.close()
asyncio.run(main())
And a client for it:
import asyncio
from aiopquic.asyncio import QuicConnectionProtocol, connect
from aiopquic.quic.configuration import QuicConfiguration
from aiopquic.quic.events import StreamDataReceived
class Client(QuicConnectionProtocol):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.reply = bytearray()
self.done = asyncio.Event()
def quic_event_received(self, event):
if isinstance(event, StreamDataReceived):
self.reply += event.data
if event.end_stream:
self.done.set()
async def main():
cfg = QuicConfiguration(is_client=True, alpn_protocols=["echo"],
server_name="localhost", cafile="ca.pem")
async with connect("localhost", 4433, configuration=cfg,
create_protocol=Client) as client:
quic = client._quic
sid = quic.get_next_available_stream_id()
quic.send_stream_data(sid, b"hello", end_stream=True)
await client.done.wait()
print(bytes(client.reply))
asyncio.run(main())
Payloads are opaque bytes; the library imposes no framing. send_stream_data is all-or-nothing:
BufferError means the stream's TX ring is full and nothing was taken, so resend the same buffer,
or await send_stream_data_drained(...) to wait. DATAFLOW.md covers the
backpressure model.
Clients verify the server's certificate chain and name against certifi's
roots. A private CA goes in QuicConfiguration.cafile (or
load_verify_locations(cafile=...)), and connect_webtransport(ca_file=...)
takes the same file. To accept any certificate, such as a self-signed test
server, set verify_mode=ssl.CERT_NONE or pass
connect_webtransport(verify_peer=False).
Clients offer a classic key share first; QuicConfiguration.key_exchange_groups
(or start(key_exchange_groups=...)) puts KEX_X25519MLKEM768 first for a
post-quantum key exchange with servers that support it.
WebTransport
The server calls handler(session) once per accepted CONNECT. session.events() yields new
streams, datagrams and the session's close; receive_stream_data(sid) yields one stream's data
up to its FIN or reset.
import asyncio
from aiopquic.asyncio.webtransport import serve_webtransport
from aiopquic.quic.events import (WebTransportNewStream,
WebTransportStreamDataReceived)
async def echo(session, sid):
async for ev in session.receive_stream_data(sid):
if isinstance(ev, WebTransportStreamDataReceived):
session.send_stream_data(sid, bytes(ev.data), end_stream=ev.end_stream)
async def handler(session): # once per accepted CONNECT
tasks = set()
async for ev in session.events():
if isinstance(ev, WebTransportNewStream):
task = asyncio.create_task(echo(session, ev.stream_id))
tasks.add(task)
task.add_done_callback(tasks.discard)
async def main():
server = await serve_webtransport("::", 4433, "/echo", handler=handler,
cert_file="cert.pem", key_file="key.pem")
try:
await asyncio.Future()
finally:
server.close()
asyncio.run(main())
import asyncio
from aiopquic.asyncio.webtransport import connect_webtransport
from aiopquic.quic.events import WebTransportStreamDataReceived
async def main():
async with connect_webtransport("localhost", 4433, "/echo",
ca_file="ca.pem") as wt:
sid = await wt.create_stream()
wt.send_stream_data(sid, b"hello", end_stream=True)
reply = bytearray()
async for ev in wt.receive_stream_data(sid):
if isinstance(ev, WebTransportStreamDataReceived):
reply += ev.data
print(bytes(reply))
asyncio.run(main())
connect_webtransport() and serve_webtransport() without configuration= run picoquic's
defaults, not QuicConfiguration's; pass one to set congestion control, flow-control windows and
the rest.
Raw QUIC and WebTransport on one port
serve_dispatch routes each connection by ALPN: raw QUIC to create_protocol, h3 to the
WebTransport handler. With the Echo protocol and handler above:
server = await serve_dispatch("::", 4433, configuration=cfg, create_protocol=Echo,
wt_path="/echo", wt_handler=handler)
Low-level binding: TransportContext
The asyncio API runs on TransportContext, which owns one picoquic instance and its network
thread. Use it directly only to share one transport between WebTransport sessions
(connect_webtransport(..., transport=t), serve_webtransport(..., transport=t)) or in tests;
the rest of aiopquic._binding._transport is internal.
from aiopquic._binding._transport import TransportContext
server = TransportContext()
server.start(port=4433, cert_file="cert.pem", key_file="key.pem", alpn="echo", is_client=False)
client = TransportContext()
client.start(port=0, alpn="echo", is_client=True)
client.create_client_connection("127.0.0.1", 4433, sni="localhost", alpn="echo")
Development
uv pip install -e '.[dev]' # or: pip install -e '.[dev]'
python -m pytest tests/ -v -m "not interop and not native"
# picoquic's own picoquic_ct / picohttp_ct suites, run as subprocesses (catches upstream
# regressions on a submodule update)
python -m pytest tests -m native
# Microbenches (opt-in)
python -m pytest tests/bench
Test-suite coverage, benchmark methodology, performance builds (AIOPQUIC_PERF=1, io_uring scaffolding), and runtime tuning are documented in PERFORMANCE.md.
Known Limitations
- Free-threaded Python (3.14t) not yet supported -- the TX-ring producer side,
TransportContextlifecycle, and the WebTransport engine state currently rely on the GIL for serialization. FT support deferred until a per-context locking audit lands.
TODO
- Windows support (eventfd alternative — IOCP / WSAEventSelect on the wake-fd path)
- Free-threaded Python (3.14t) support after producer-side locking audit
- Datagram benches: latency percentiles, payload-size sweep, loss / jitter under load (today's
bench_datagramis fire-and-count throughput only) - Pure stream open/close microbench (lifecycle rate without payload, separate from
bench_stream_churn_highlevelwhich bundles writes + FIN) - QUIC interop runner: client and server endpoints proposed upstream in quic-interop-runner#518, awaiting review
Acknowledgments
aiopquic is built on picoquic: QUIC, HTTP/3 and WebTransport all come from it. Many thanks to Christian Huitema, picoquic's author, and to its contributors for that work.
Resources
- picoquic -- QUIC implementation by Christian Huitema
- picotls -- TLS 1.3 implementation
- Media Over QUIC Working Group
A Marz Research project.
Author: G. S. Marzot <gmarzot@marzresearch.net>
License
MIT License -- see LICENSE
Metadata
Release files for aiopquic 0.7.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| aiopquic-0.7.1.tar.gz | 3.9 MB | Without provenance | Details for aiopquic-0.7.1.tar.gz |
Built distributions (wheels)
Total release size: 51.0 MB
Release files / aiopquic-0.7.1.tar.gz
| Download URL | aiopquic-0.7.1.tar.gz |
|---|---|
| Size | 3.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
03b0987b12b2b9327f1965f498dace42a6e3f7bcb091ed5433e60a000cedfddc
|
|
BLAKE2b-256 checksum How to use checksums |
75c420525c7efbdc3de03dec862db5725069ca70b54b39c9a87a30bdb5605596
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp314-cp314-manylinux_2_34_x86_64.whl
| Download URL | aiopquic-0.7.1-cp314-cp314-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 5.8 MB |
| Tags | CPython 3.14 Linux glibc 2.34+ x86-64 |
|
SHA-256 checksum How to use checksums |
c9e0a771c30f57461543250f6f7f0855b5140ab58a94adfbe8b1aaee1ff4e365
|
|
BLAKE2b-256 checksum How to use checksums |
5d4ddc6f8bb532c15f08db67a92febb6391a2e96f1a937058d0bb86315c49592
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp314-cp314-manylinux_2_34_aarch64.whl
| Download URL | aiopquic-0.7.1-cp314-cp314-manylinux_2_34_aarch64.whl |
|---|---|
| Size | 5.6 MB |
| Tags | CPython 3.14 Linux glibc 2.34+ ARM64 |
|
SHA-256 checksum How to use checksums |
17116f650d008e99a4528b103908ae851f13f82253eb6512dd1b9a94e81c24c4
|
|
BLAKE2b-256 checksum How to use checksums |
5e60c939176237b77d779b2c9f63c62c84f5886cc80f00175e85c415e532ce19
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp314-cp314-macosx_15_0_arm64.whl
| Download URL | aiopquic-0.7.1-cp314-cp314-macosx_15_0_arm64.whl |
|---|---|
| Size | 4.3 MB |
| Tags | CPython 3.14 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
ca216357bba60790a64df88d324fa1ca4e90e840a489051671fa28081dae0237
|
|
BLAKE2b-256 checksum How to use checksums |
e599feb6ecdd88b5ce969aa4da0d49c8ad2129e6996a4753cfff08aea809d6ae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp313-cp313-manylinux_2_34_x86_64.whl
| Download URL | aiopquic-0.7.1-cp313-cp313-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 5.8 MB |
| Tags | CPython 3.13 Linux glibc 2.34+ x86-64 |
|
SHA-256 checksum How to use checksums |
134fe725879636f4ec433e767d1a118e11cefe6631a3eab7a02b4842734c79c8
|
|
BLAKE2b-256 checksum How to use checksums |
b94d920ccddfd268656b60ebfbb88f4cf21b090f434bec7317f601d6b1705775
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp313-cp313-manylinux_2_34_aarch64.whl
| Download URL | aiopquic-0.7.1-cp313-cp313-manylinux_2_34_aarch64.whl |
|---|---|
| Size | 5.5 MB |
| Tags | CPython 3.13 Linux glibc 2.34+ ARM64 |
|
SHA-256 checksum How to use checksums |
c554f2fd85795cbeea2f3e281c4a3d675e2a0ac9fff5507d1631f569b1bfaf91
|
|
BLAKE2b-256 checksum How to use checksums |
b56a5cd3c6a3ea733ed9d34e84247b1d9cdf3815bf82bb73df23ba8eafbdf1bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp313-cp313-macosx_15_0_arm64.whl
| Download URL | aiopquic-0.7.1-cp313-cp313-macosx_15_0_arm64.whl |
|---|---|
| Size | 4.3 MB |
| Tags | CPython 3.13 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
1bf65853f25426b429afad5945dff69645dead9f1c27f4aad328984b69bfcb73
|
|
BLAKE2b-256 checksum How to use checksums |
a1216d2e0584b9586916f99b9f42de2839b48d7096bf41d6ecf307b3790f0833
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp312-cp312-manylinux_2_34_x86_64.whl
| Download URL | aiopquic-0.7.1-cp312-cp312-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 5.9 MB |
| Tags | CPython 3.12 Linux glibc 2.34+ x86-64 |
|
SHA-256 checksum How to use checksums |
66df7851e7a7f28d76d832e8cdbba9a5dad9dc965809fbc2a28b6fb1e176e7b7
|
|
BLAKE2b-256 checksum How to use checksums |
279abc65922a99f3fad98e188106984a4ac8d43f813d1e369682389d6be22b0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp312-cp312-manylinux_2_34_aarch64.whl
| Download URL | aiopquic-0.7.1-cp312-cp312-manylinux_2_34_aarch64.whl |
|---|---|
| Size | 5.5 MB |
| Tags | CPython 3.12 Linux glibc 2.34+ ARM64 |
|
SHA-256 checksum How to use checksums |
3c6c228dc2e4643efd1ebd5fbdc238eff682be7c8c7b53ec5ac2323fc1bdac40
|
|
BLAKE2b-256 checksum How to use checksums |
67b42e1d3c82c9c44983c861edb7d1376e15026144d0162067efb24abfdcb22a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Release files / aiopquic-0.7.1-cp312-cp312-macosx_15_0_arm64.whl
| Download URL | aiopquic-0.7.1-cp312-cp312-macosx_15_0_arm64.whl |
|---|---|
| Size | 4.3 MB |
| Tags | CPython 3.12 macOS 15.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2ddef2a704bd23721a223683681fdb11533ed31f974985eef3a09295b206ebcb
|
|
BLAKE2b-256 checksum How to use checksums |
09dd216dc35d50f0258891f73c48747b44bf39dfa7ef0e0eb1310597bf5d9d65
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.10
|
Security for aiopquic 0.7.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.