picows in UNICORN Binance WebSocket API: Up to 2× the Throughput, Opt-In for Now
UBWA 2.16.0 can run on picows, a Cython WebSocket implementation, instead of websockets. Benchmarks, a 24 h soak against live Binance, three bugs found on the way, and why the default has not changed yet.

UNICORN Binance WebSocket API (UBWA) has used websockets since day one.
Since UBWA 2.16.0, you can switch a manager instance to picows, a Cython WebSocket implementation.
For typical Binance messages below ~1 KB, picows delivers roughly 1.7–2× the throughput with significantly lower CPU cost. At normal trading-bot message rates, however, you probably won't notice a difference.
That's why picows is opt-in for now.
Why picows?
websockets is a very good pure-Python library. For a few hundred messages per second it is nowhere near the bottleneck.
But UBWA is also used for hundreds of depth@100ms subscriptions, multiplexed connections and local depth caches processing every diff. At those rates, per-message overhead starts to matter.
picows moves much of the WebSocket protocol handling into Cython.
It also provides a picows.websockets compatibility layer with the familiar API: connect(), recv(), send(), close() and matching exceptions.
That made the UBWA integration small.
Switching libraries
One parameter:
from unicorn_binance_websocket_api import BinanceWebSocketApiManager
ubwa = BinanceWebSocketApiManager(
exchange="binance.com"
)
ubwa = BinanceWebSocketApiManager(
exchange="binance.com",
websocket_library="picows"
)
picows is optional:
pip install --upgrade "unicorn-binance-websocket-api[picows]>=2.16.0"
Streams, WebSocket API, userData streams, subscribe/unsubscribe, reconnects, signals and proxies use the same UBWA code path.
There is also no silent fallback.
If you select "picows" without installing it, UBWA raises ImportError. Unknown values raise ValueError.
If a bot says it is running on picows, it should actually be running on picows.
Benchmark
The benchmark runs both libraries through the full UBWA stack against a local server sending Binance-shaped messages.
Python 3.13, websockets 16.0, picows 2.1.3, x86_64 Linux, output_default="raw_data", median of three runs:
| Scenario | ~msg size | websockets msgs/s | picows msgs/s | speedup | websockets CPU µs/msg | picows CPU µs/msg |
|---|---|---|---|---|---|---|
| aggTrade | 0.2 KB | 201,912 | 403,316 | 2.00x | 5.1 | 2.5 |
| kline | 0.3 KB | 195,460 | 371,019 | 1.90x | 5.2 | 2.9 |
| depth20 | 1.0 KB | 153,187 | 259,960 | 1.70x | 6.8 | 4.1 |
| depth diff | 9.1 KB | 64,172 | 67,972 | 1.06x | 16.3 | 15.4 |
| !ticker@arr | 453.9 KB | 1,768 | 1,662 | 0.94x | 608.7 | 641.0 |
| multiplex mix | 0.2 KB | 180,406 | 334,188 | 1.85x | 5.7 | 3.2 |
The pattern is simple.
For small Binance messages, picows is clearly faster. Around 10 KB the difference mostly disappears. On the huge 450 KB !ticker@arr payload, picows is slightly slower.
With output_default="dict" and JSON parsing included, the advantage for small messages is still around 1.4–1.7×.
Against live Binance at only a few hundred messages per second, there is effectively no difference. Both libraries spend most of their time waiting for data.
If that is your workload, switching gives you little.
If you push tens of thousands of messages per second through one process, it matters.
The benchmark found a UBWA bottleneck first
The first benchmark showed only about 1.4× improvement.
Profiling found why.
UBWA had 18 logger.debug() f-strings formatted for every message even when debug logging was disabled, seven lock acquisitions per message where one was enough, and heartbeat/stop checks running twice.
After removing that overhead:
websockets: 116k → 202k msgs/s
picows: 163k → 403k msgs/s
picows did not suddenly get faster.
UBWA stopped hiding its speed.
The details are documented in context/stream-loop.md.
Why I did not use the native picows API
picows also has a native listener API based on ws_connect() and WSListener.
I benchmarked it too.
On 0.2 KB messages it managed roughly 499k msgs/s versus 485k through the compatibility API. On 9 KB messages the difference was around 13%.
Inside the full UBWA stack that would translate to less than 5% end-to-end.
In return, UBWA would need a second connection implementation of roughly 300–400 lines.
Not worth it.
UBWA stays on the compatibility API.
Failure-path testing found a real bug
Performance benchmarks are easy. Failure handling is more interesting.
The test suite now runs both libraries through scenarios including:
server-side closes and reconnects
fragmented frames
450 KB payloads
messages above
max_sizeserver pings
Unicode
rejected handshakes
WebSocket API round trips
keepalive timeouts
The rejected-handshake test found a real compatibility issue.
When Binance returns HTTP 429 or 404 during the WebSocket upgrade, UBWA reads the status from InvalidStatus.
websockets exposed response.status_code.
picows 2.1.x exposed response.status.
UBWA hit an AttributeError, the stream thread died and nothing useful was logged.
I reported it upstream as tarasko/picows#108.
picows 2.2.0 fixed it the next day.
Proxy support got simpler too
picows 2.3.0 added native HTTP, HTTPS, SOCKS4 and SOCKS5 proxy support.
websockets has supported the same since 15.0.
UBWA previously handled SOCKS5 itself using PySocks and a blocking handshake inside the event loop.
That code is now gone.
UBWA simply passes the proxy URL to the selected WebSocket library:
ubwa = BinanceWebSocketApiManager(
exchange="binance.com",
proxy="socks5://user:pass@127.0.0.1:9050"
)
ubwa = BinanceWebSocketApiManager(
exchange="binance.com",
proxy="http://127.0.0.1:3128"
)
Both libraries now support:
http://
https://
socks4://
socks5://
The old socks5_proxy_server parameters still work and are converted internally.
Testing also found two UBWA issues in the old proxy path:
rejected SOCKS5 credentials could kill a stream thread without a useful log message
TLS certificate verification was not actually enabled on the proxy path despite the option defaulting to
True
Both are fixed.
One difference remains: websockets currently does not URL-decode proxy credentials such as p%40ss, while python-socks/picows does. I reported that as python-websockets/websockets#1761.
UBWA rejects affected credentials up front when using websockets instead of entering a reconnect loop.
The performance and soak tests below were run with picows 2.1.3. During integration, 2.2.0 fixed the handshake compatibility issue and 2.3.0 added native proxy support. That is why UBWA requires picows 2.3.0.
24 hours against live Binance
Local tests do not tell you what happens after hours of reconnects, traffic spikes and memory allocation.
So both libraries ran for 24 hours in parallel against binance.com with identical subscriptions.
Load:
!ticker@arr!miniTicker@arraggTrade
trade
depth20@100mskline_1mbookTicker
depth@100ms
across up to 50 USDT markets.
Host: 8 cores, 12 GB RAM, Python 3.13.5.
| picows | websockets | |
|---|---|---|
| Messages / data | 138.8 M / 49.9 GB | 137.9 M / 49.7 GB |
| Avg / peak msgs/s | 1,606 / 8,260 | 1,596 / 7,695 |
| RSS start → end | 62 → 126 MB | 63 → 149 MB |
| CPU avg | 9.5 % | 12.8 % |
| Reconnects (3 streams) | 2 / 80 / 2 | 2 / 88 / 2 |
| Reconnect duration | 5–6 s | 5–6 s |
| Max seconds without data | 5 s | 5 s |
| Errors / stalls / unrepairable streams | 0 / 0 / 0 | 0 / 0 / 0 |
138 million messages later: no picows-specific failure, about 26% lower average CPU usage and 23 MB less RSS at the end of the run.
The high reconnect count on one stream occurred almost entirely during a four-hour high-load window.
Both implementations disconnected in the same seconds with keepalive ping timeouts and recovered within five to six seconds.
That strongly points to a shared external or workload-related cause rather than either WebSocket implementation.
Memory rose during traffic peaks and then remained flat for the final hours despite further reconnects.
No sign of a reconnect leak.
Why websockets is still the default
Because 138 million messages and a 24-hour soak are good evidence.
They are not the same as thousands of users running picows for months on different systems.
The compatibility layer is younger than websockets, and issue #108 already showed that small API differences can matter.
So for now:
websockets remains the default. picows is opt-in.
If picows holds up across enough real-world setups, the default can flip later.
The switch will remain either way.
Try it
pip install --upgrade "unicorn-binance-websocket-api[picows]>=2.16.0"
from unicorn_binance_websocket_api import BinanceWebSocketApiManager
ubwa = BinanceWebSocketApiManager(
exchange="binance.com",
websocket_library="picows"
)
ubwa.create_stream(
["aggTrade", "depth20@100ms"],
["btcusdt", "ethusdt", "solusdt"]
)
Run your workload with both libraries and compare CPU and memory usage.
If you have numbers, edge cases or failures, post them in issue #477.
That feedback will decide whether picows becomes the default.
The reasoning behind these decisions — including the native picows implementation I chose not to build — lives next to the code in context/, maintained with Keep the Why.
I hope you found this informative and useful.
Follow me on GitHub, Bluesky, Mastodon, X, and LinkedIn, or join Telegram for updates on my latest publications. Constructive feedback is always appreciated.
Thank you for reading, and happy coding! ¯\_(ツ)_/¯





