How picows Became the Fastest WebSocket Engine for asyncio: An Architecture Without Compromises
The picows library has rewritten the rules of WebSocket performance in Python, ditching convenience for raw speed. Forget async for and automatic message assembly—get direct access to frames via a callback interface, zero-copy processing, and full control over buffers. For algorithmic trading, where every millisecond costs money, this isn't just an optimization—it's a necessity.
Why Standard Solutions Fall Short
When it comes to high-frequency data processing—for example, market updates from crypto exchanges—aiohttp and websockets deliver unacceptable latency. Under a load of 10–20 thousand messages per second (200 bytes each), they start piling up data in internal queues. The delay between receiving a packet from the network and delivering it to your business logic can hit 100 ms—fatal for trading strategies.
The issue isn't Python itself, but the architecture:
- Data gets copied multiple times: from socket → parser buffer → queue → user coroutine.
- Parsers run in Python (except for the partially rewritten C parser in aiohttp), adding overhead.
- TEXT messages get converted to str, requiring extra memory allocation.
- Sending large messages (e.g., 1 MB) requires copying the entire payload just to add a 2–14 byte header.
This isn't just slow—it's predictably inefficient. Especially when using asyncio.Protocol, which always hands over new bytes objects instead of the faster BufferedProtocol that lets you work with external buffers.
picows Architecture: Zero-Copy and Minimal Abstraction
Picows is built on principles that other libraries would consider too low-level:
- Uses asyncio.BufferedProtocol to minimize copying on reads.
- A C parser breaks down the frame header and immediately passes payload boundaries to user code—no intermediate objects.
- Users work directly with memoryview or bytearray—no str conversion unless needed.
- Sending uses send_reuse_external_bytearray—the header is written straight into reserved space before the payload, with masking applied in-place.
Ditching conveniences wasn't easy, but it's worth it:
- No async for—instead, a callback on_ws_frame.
- No automatic multi-frame message assembly—users decide how to concatenate them.
- No built-in permessage-deflate support—handle compression at the app level.
- send() isn't async—if the socket is busy, data goes into a queue without blocking.
This isn't a bug, it's a feature: calling an async function on every frame adds tens of microseconds of latency. Picows lets you process frames without any asyncio context switches.
Code Examples: Client and Server with picows
Minimal client:
import asyncio
from picows import ws_connect, WSFrame, WSTransport, WSListener, WSMsgType, WSCloseCode
class ClientListener(WSListener):
def on_ws_connected(self, transport: WSTransport):
transport.send(WSMsgType.TEXT, b"Hello world")
def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
print(f"Echo reply: {frame.get_payload_as_ascii_text()}")
transport.send_close(WSCloseCode.OK)
transport.disconnect()
async def main():
transport, client = await ws_connect(ClientListener, "ws://127.0.0.1:9001")
await transport.wait_disconnected()
if __name__ == '__main__':
asyncio.run(main())
Echo server:
import asyncio
from picows import ws_create_server, WSFrame, WSTransport, WSListener, WSMsgType, WSUpgradeRequest
class ServerClientListener(WSListener):
def on_ws_connected(self, transport: WSTransport):
print("New client connected")
def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
if frame.msg_type == WSMsgType.CLOSE:
transport.send_close(frame.get_close_code(), frame.get_close_message())
transport.disconnect()
else:
transport.send(frame.msg_type, frame.get_payload_as_memoryview())
async def main():
def listener_factory(r: WSUpgradeRequest):
return ServerClientListener()
server: asyncio.Server = await ws_create_server(listener_factory, "127.0.0.1", 9001)
for s in server.sockets:
print(f"Server started on {s.getsockname()}")
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
Notice: no await inside on_ws_frame. Processing happens synchronously, without context switches. For async behavior, use create_task or Queue—but that's a deliberate developer choice, not forced by the architecture.
Benchmarks: Where picows Leaves Competitors in the Dust
Tests show the difference in RPS (requests per second) for echo exchanges with messages of various sizes:
- For small messages (64–512 bytes), picows beats aiohttp by 3–5x, websockets by 8–10x.
- For large messages (1 MB+), the lead grows even more—up to 20x, thanks to zero-copy sending.
- Latency between receiving and processing a frame stays under 100 microseconds even at 50K RPS.
Key factors:
- No unnecessary copying—data is read and written in-place.
- Minimal object creation—no intermediate str, list, or dict.
- Zero abstraction on frame handling—callback fires right after header parsing.
- Optimized C code—all parsing and frame building happens in C, without Python interpreter involvement.
Key Takeaways
- Picows isn't for everyone—it's for those who need maximum performance over a convenient API.
- Architecture follows asyncio's transport model: transport/protocol, not high-level abstractions.
- Zero-copy processing and direct memory access are the killer advantages over competitors.
- Skipping async in the hot path slashed latency by orders of magnitude.
- TLS support and advanced scenarios (multi-frame, masking) are handled without speed loss.
When to Choose picows—and When Not
Use picows if:
- Latency under 1 ms is critical for handling thousands of messages per second.
- You're willing to trade convenience for control over memory and performance.
- Your logic can run without constant async/await in the frame handler.
- You're dealing with binary data or JSON that can be parsed directly from memoryview.
Skip it if:
- You prioritize simple API and quick starts.
- You expect automatic multi-frame message assembly.
- You need built-in compression or WebSocket extensions.
- Your load is light (< 1K RPS) and you don't want to complicate your architecture.
Picows isn't a replacement for aiohttp or websockets. It's a specialized tool for edge cases where every CPU cycle counts. For algorithmic trading, game servers, IoT gateways, and high-frequency data pipelines—it's one of the best options in the Python ecosystem.
— Editorial Team
No comments yet.