Skip to main content

Command Palette

Search for a command to run...

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.

Updated
8 min readView as Markdown
picows in UNICORN Binance WebSocket API: Up to 2× the Throughput, Opt-In for Now
O
I build systems that work — technically sound, security-first, and actually useful to the people who depend on them. Creator of the UNICORN Binance Suite — six open-source Python libraries with 3.3M+ downloads and 390+ dependent public projects — and Keep the Why, an open-source agent skill that preserves the reasoning behind engineering decisions alongside the code. Currently pioneering AI-driven open-source maintenance: running a controlled AI agent that maintains production code and documenting what this shift means for engineering teams. Vienna, Austria 🇦🇹

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_size

  • server 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@arr

  • aggTrade

  • trade

  • depth20@100ms

  • kline_1m

  • bookTicker

  • 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! ¯\_(ツ)_/¯

UNICORN Binance Suite

Part 1 of 18

Open source Python libraries for automated trading on Binance — WebSocket streams, REST API, local order books, trailing stop loss, and Kubernetes-scale depth caching. Engineering decisions, real-world findings, and lessons from production.

Up next

I Created 2013 Binance Order Books on Kubernetes with 2 Replicas in 25 Minutes — Then Stress-Tested the REST API

Using UBDCC on six low-cost Vultr nodes to synchronize 4026 replicated Binance Spot and Futures DepthCaches, monitor the cluster, and push the REST API with Grafana Cloud k6.