Ultra-low latency WebSocket library for Rust, designed for high-frequency trading (HFT) applications and real-time systems. Fully compatible with Tokio, Compio, and Axum.
Will be used in Sockudo, a high-performance Pusher-compatible WebSocket server.
Benchmarked using web-socket-benchmark (100,000 iterations of "Hello, World!" message):
| Library | Send | Echo | Recv | Total |
|---|---|---|---|---|
| sockudo-ws | 1.2ms | 5.0ms | 3.1ms | 10.2ms |
| fastwebsockets | 3.3ms | 5.7ms | 3.0ms | 12.0ms |
| web-socket | 2.1ms | 6.8ms | 3.3ms | 12.2ms |
| soketto | 5.8ms | 17.6ms | 9.7ms | 33.1ms |
| tokio-tungstenite | 6.4ms | 18.2ms | 10.2ms | 34.8ms |
sockudo-ws is ~17% faster than the next fastest Rust WebSocket library!
How to reproduce
# Clone the benchmark repository
git clone https://github.com/nurmohammed840/web-socket-benchmark
cd web-socket-benchmark
# Add sockudo-ws to the benchmark suite, then run:
cargo benchThe benchmark measures:
- Send: Time to send 100,000 "Hello, World!" messages from client to server
- Echo: Time to send and receive 100,000 messages (round-trip)
- Recv: Time to receive 100,000 messages from server to client
Environment: AMD Ryzen 9 7950X, 32GB RAM, Linux 6.18, Rust 1.82
Benchmarked against uWebSockets, the industry standard for high-performance WebSockets, using the fastwebsockets benchmark suite:
| Test Case | sockudo-ws | uWebSockets | Ratio |
|---|---|---|---|
| 512 bytes, 100 connections | 232,712 msg/s | 227,973 msg/s | 1.02x |
| 1024 bytes, 100 connections | 232,072 msg/s | 224,498 msg/s | 1.03x |
| 512 bytes, 500 connections | 231,135 msg/s | 222,493 msg/s | 1.03x |
| 1024 bytes, 500 connections | 222,578 msg/s | 216,833 msg/s | 1.02x |
How to reproduce
# Clone the fastwebsockets repository
git clone https://github.com/denoland/fastwebsockets
cd fastwebsockets/benches
# Follow the instructions in the benchmark directory to run
# the comparison between sockudo-ws and uWebSocketsEnvironment: AMD Ryzen 9 7950X, 32GB RAM, Linux 6.18, Rust 1.82, uWebSockets v20.64
sockudo-ws matches or exceeds uWebSockets performance while providing a safe, ergonomic Rust API.
- SIMD Acceleration: AVX2/AVX-512/SSE2/NEON/AltiVec/LSX for frame masking and UTF-8 validation
- Zero-Copy Parsing: Direct buffer access without intermediate allocations
- Write Batching (Corking): Minimizes syscalls via vectored I/O
- permessage-deflate: Full compression support with shared/dedicated compressors
- Lock-Free Split Streams: True concurrent read/write using OS-level stream splitting (zero mutex contention)
- Runtime-Separated APIs: Tokio support via
tokio-runtime, native Compio support viacompio-runtime - Pub/Sub System: High-performance topic-based messaging with sender exclusion
- HTTP/2 WebSocket: RFC 8441 Extended CONNECT protocol support
- HTTP/3 WebSocket: RFC 9220 WebSocket over QUIC support
- io_uring: Linux high-performance async I/O (combinable with HTTP/2 and HTTP/3)
- Autobahn Compliant: Passes all 517 Autobahn test suite cases
- Fuzz Tested: Comprehensive fuzzing with libFuzzer
Add to your Cargo.toml:
[dependencies]
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws" }
# With compression
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["permessage-deflate"] }
# Default Tokio runtime with HTTP/2 support
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["http2"] }
# Default Tokio runtime with HTTP/3 support
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["http3"] }
# Tokio runtime without default features
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", default-features = false, features = ["tokio-runtime", "http2", "fastrand"] }
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", default-features = false, features = ["tokio-runtime", "http3", "fastrand"] }
# With io_uring (Linux only)
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["io-uring"] }
# With native Compio runtime support
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", default-features = false, features = ["compio-runtime", "fastrand"] }
# With Compio and HTTP/2 or HTTP/3 transports
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", default-features = false, features = ["compio-runtime", "http2", "fastrand"] }
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", default-features = false, features = ["compio-runtime", "http3", "fastrand"] }
# With TLS (rustls)
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["rustls-webpki-roots"] }
rustls = { version = "0.23", default-features = false, features = [
"aws-lc-rs",
"logging",
"std",
"tls12",
] }
# With TLS (native-tls)
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["native-tls"] }
# All transports
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["all-transports"] }
# Everything
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["full"] }
# With mimalloc allocator (recommended for production)
sockudo-ws = { git = "https://github.com/sockudo/sockudo-ws", features = ["mimalloc"] }Runtime selection is independent from protocol selection. Enable exactly the runtime API you want, then add http2 and/or http3 for extended CONNECT transports.
| Runtime feature | HTTP/1.1 WebSocket | HTTP/2 WebSocket | HTTP/3 WebSocket |
|---|---|---|---|
tokio-runtime |
WebSocketStream / WebSocketClient<Http1> |
WebSocketClient<Http2>, WebSocketServer<Http2>, MultiplexedConnection<Http2> |
WebSocketClient<Http3>, WebSocketServer<Http3>, MultiplexedConnection<Http3> |
compio-runtime |
compio::accept_async, compio::connect_async |
compio::serve_http2, compio::connect_http2, compio::connect_http2_multiplexed |
compio::CompioHttp3Server, compio::connect_http3, compio::connect_http3_multiplexed |
The http2 and http3 features do not select a runtime. For example, default-features = false, features = ["compio-runtime", "http2"] builds Compio HTTP/2 support without enabling the Tokio runtime API.
use futures_util::{SinkExt, StreamExt};
use sockudo_ws::{Config, Message, WebSocketStream};
use tokio::net::TcpStream;
async fn handle(stream: TcpStream) {
// After WebSocket handshake...
let mut ws = WebSocketStream::server(stream, Config::default());
while let Some(msg) = ws.next().await {
match msg.unwrap() {
Message::Text(text) => {
ws.send(Message::text(text)).await.unwrap();
}
Message::Binary(data) => {
ws.send(Message::Binary(data)).await.unwrap();
}
Message::Close(_) => break,
_ => {}
}
}
}sockudo-ws uses tokio::io::split() for true concurrent I/O with zero mutex contention:
use sockudo_ws::{Config, Message, WebSocketStream};
use tokio::sync::mpsc;
async fn handle(stream: TcpStream) {
let ws = WebSocketStream::server(stream, Config::default());
// Split into independent read/write halves
// Reader and writer can operate 100% concurrently!
let (mut reader, mut writer) = ws.split();
// Writer task - NEVER blocks reader
let (tx, mut rx) = mpsc::channel::<Message>(32);
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
writer.send(msg).await.unwrap();
}
});
// Reader loop - NEVER blocks writer
while let Some(msg) = reader.next().await {
match msg.unwrap() {
Message::Text(text) => {
tx.send(Message::text(text)).await.unwrap();
}
Message::Close(_) => break,
_ => {}
}
}
}Why This is Fast:
- ✅ Zero mutex contention - reader and writer operate independently
- ✅ OS-level splitting - leverages tokio's optimized
ReadHalfandWriteHalf - ✅ True concurrency - can read and write simultaneously without blocking
- ✅ Control frame coordination - Ping/Pong/Close handled via lightweight mpsc channel
HTTP/1.1 clients can attach application headers to the WebSocket upgrade request. Sockudo validates
header syntax and rejects attempts to override handshake-managed headers such as Host, Upgrade,
and Sec-WebSocket-Key.
use sockudo_ws::{Config, Http1};
use sockudo_ws::client::WebSocketClient;
use tokio::net::TcpStream;
async fn connect() -> sockudo_ws::Result<()> {
let stream = TcpStream::connect("example.com:80").await?;
let headers = vec![
("Authorization".to_string(), "Bearer token".to_string()),
("User-Agent".to_string(), "my-client".to_string()),
];
let client = WebSocketClient::<Http1>::new(Config::default());
let (_websocket, _handshake) = client
.connect_with_headers(
stream,
"example.com",
"/ws",
None,
Some(&headers),
)
.await?;
Ok(())
}Compio applications can use sockudo_ws::compio::connect_async_with_headers with the same header
representation and validation rules.
Compio uses completion-based I/O, so sockudo-ws exposes a native async-method API behind compio-runtime:
use sockudo_ws::compio::{accept_async, connect_async, net::{TcpListener, TcpStream}};
use sockudo_ws::{Config, Message};
#[sockudo_ws::compio::main]
async fn main() -> sockudo_ws::Result<()> {
let listener = TcpListener::bind("127.0.0.1:9001").await?;
let server = sockudo_ws::compio::runtime::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let (mut ws, _) = accept_async(stream, Config::default()).await.unwrap();
while let Some(msg) = ws.next().await {
let msg = msg.unwrap();
if matches!(msg, Message::Close(_)) {
break;
}
ws.send(msg).await.unwrap();
}
});
let stream = TcpStream::connect("127.0.0.1:9001").await?;
let (mut ws, _) = connect_async(
stream,
"127.0.0.1:9001",
"/",
None,
Config::default(),
)
.await?;
ws.send_text("hello").await?;
let _ = ws.next().await;
server.await.unwrap();
Ok(())
}Compio uses the same http2 and http3 transport features as Tokio. Only the runtime feature changes.
use sockudo_ws::compio::{
connect_http2, serve_http2,
net::{TcpListener, TcpStream},
};
use sockudo_ws::Config;
#[sockudo_ws::compio::main]
async fn main() -> sockudo_ws::Result<()> {
let listener = TcpListener::bind("127.0.0.1:9002").await?;
let addr = listener.local_addr()?;
let server = sockudo_ws::compio::runtime::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
serve_http2(stream, Config::default(), |mut ws, _req| async move {
if let Some(Ok(msg)) = ws.next().await {
ws.send(msg).await.unwrap();
}
})
.await
.unwrap();
});
let stream = TcpStream::connect(addr).await?;
let mut ws = connect_http2(
stream,
&format!("https://localhost:{}/chat", addr.port()),
None,
Config::default(),
)
.await?;
ws.send_text("hello over h2").await?;
let _ = ws.next().await;
server.await.unwrap();
Ok(())
}use sockudo_ws::compio::{CompioHttp3Server, connect_http3_multiplexed};
use sockudo_ws::Config;
async fn http3_example(
server_addr: std::net::SocketAddr,
server_tls: rustls::ServerConfig,
client_tls: rustls::ClientConfig,
) -> sockudo_ws::Result<()> {
let server = CompioHttp3Server::bind(server_addr, server_tls, Config::default()).await?;
let addr = server.local_addr()?;
let server_task = sockudo_ws::compio::runtime::spawn(async move {
server
.serve(|mut ws, _req| async move {
if let Some(Ok(msg)) = ws.next().await {
ws.send(msg).await.unwrap();
}
})
.await
.unwrap();
});
let mut mux = connect_http3_multiplexed(addr, "localhost", client_tls, Config::default()).await?;
let mut chat = mux.open_websocket("/chat", None).await?;
let mut notifications = mux.open_websocket("/notifications", None).await?;
chat.send_text("hello h3").await?;
notifications.send_text("ping").await?;
mux.close();
server_task.await.unwrap();
Ok(())
}sockudo-ws includes a high-performance pub/sub system for topic-based messaging, inspired by uWebSockets/Bun:
use sockudo_ws::pubsub::PubSub;
use sockudo_ws::Message;
use tokio::sync::mpsc;
// Create pub/sub system
let pubsub = PubSub::new();
// Create a subscriber with a message channel
let (tx, mut rx) = mpsc::unbounded_channel();
let subscriber_id = pubsub.create_subscriber(tx);
// Subscribe to topics
pubsub.subscribe(subscriber_id, "chat/general");
pubsub.subscribe(subscriber_id, "notifications");
// Publish to all subscribers
let msg = Message::text("Hello everyone!");
pubsub.publish("chat/general", msg);
// Publish excluding a specific subscriber (useful for echo prevention)
let msg = Message::text("Broadcast from user");
pubsub.publish_excluding(subscriber_id, "chat/general", msg);
// Unsubscribe from a topic
pubsub.unsubscribe(subscriber_id, "chat/general");
// Remove subscriber when connection closes
pubsub.remove_subscriber(subscriber_id);use sockudo_ws::pubsub::PubSub;
let pubsub = PubSub::new();
// Generate Pusher-style socket ID (format: "1234567890.9876543210")
let socket_id = PubSub::generate_socket_id();
// Create subscriber with custom socket ID
let (tx, rx) = mpsc::unbounded_channel();
let subscriber_id = pubsub.create_subscriber_with_id(&socket_id, tx);
// Subscribe/publish using socket ID
pubsub.subscribe_by_socket_id(&socket_id, "private-channel");
pubsub.publish_excluding_socket_id(&socket_id, "chat", Message::text("Hello"));
// Lookup subscriber by socket ID
if let Some(id) = pubsub.get_subscriber_by_socket_id(&socket_id) {
println!("Found subscriber: {:?}", id);
}- 64 Sharded Topics: Reduced lock contention for high concurrency
- Lock-Free Subscriber IDs: Atomic allocation for fast subscriber creation
- Zero-Copy Messages: Uses
Bytesfor efficient message sharing - Cache-Line Alignment: Prevents false sharing in concurrent access
- Pusher-Style String IDs: Optional string-based subscriber identifiers
- Sender Exclusion:
publish_excluding()prevents echo to the sender - Automatic Cleanup: Empty topics are removed automatically
// Get statistics
let topic_count = pubsub.topic_count();
let subscriber_count = pubsub.subscriber_count();
let messages_published = pubsub.messages_published();
let subscribers_in_topic = pubsub.topic_subscriber_count("chat/general");use axum::{Router, body::Body, extract::Request, http::{Response, StatusCode, header}, routing::get};
use futures_util::{SinkExt, StreamExt};
use hyper_util::rt::TokioIo;
use sockudo_ws::{Config, Message, WebSocketStream, handshake::generate_accept_key};
async fn ws_handler(req: Request) -> Response<Body> {
let key = req.headers().get("sec-websocket-key").unwrap().to_str().unwrap();
let accept_key = generate_accept_key(key);
tokio::spawn(async move {
if let Ok(upgraded) = hyper::upgrade::on(req).await {
let mut ws = WebSocketStream::server(TokioIo::new(upgraded), Config::default());
while let Some(Ok(msg)) = ws.next().await {
match msg {
Message::Text(text) => { ws.send(Message::text(text)).await.ok(); }
Message::Binary(data) => { ws.send(Message::Binary(data)).await.ok(); }
Message::Close(_) => break,
_ => {}
}
}
}
});
Response::builder()
.status(StatusCode::SWITCHING_PROTOCOLS)
.header(header::UPGRADE, "websocket")
.header(header::CONNECTION, "Upgrade")
.header("Sec-WebSocket-Accept", accept_key)
.body(Body::empty())
.unwrap()
}HTTP/2 WebSocket uses the Extended CONNECT protocol for multiplexed WebSocket streams over a single TCP connection.
use sockudo_ws::{WebSocketServer, Http2, Config, Message};
use futures_util::{SinkExt, StreamExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
let config = Config::builder()
.http2_max_streams(100)
.build();
let server = WebSocketServer::<Http2>::new(config);
loop {
let (stream, _) = listener.accept().await?;
// In production: wrap with TLS first
// let tls_stream = tls_acceptor.accept(stream).await?;
let server = server.clone();
tokio::spawn(async move {
server.serve(stream, |mut ws, req| async move {
println!("HTTP/2 WebSocket at: {}", req.path);
// Same API as HTTP/1.1!
while let Some(msg) = ws.next().await {
if let Ok(msg) = msg {
ws.send(msg).await.ok();
}
}
}).await.ok();
});
}
}use sockudo_ws::{WebSocketClient, Http2, Config, Message};
let client = WebSocketClient::<Http2>::new(Config::default());
let mut ws = client.connect(tls_stream, "wss://example.com/ws", None).await?;
ws.send(Message::text("Hello!")).await?;Open multiple WebSocket streams over a single HTTP/2 connection:
use sockudo_ws::{WebSocketClient, Http2, Config};
let client = WebSocketClient::<Http2>::new(Config::default());
let mut conn = client.connect_multiplexed(tls_stream).await?;
// Open multiple WebSocket streams on the same connection
let mut ws1 = conn.open_websocket("wss://example.com/chat", None).await?;
let mut ws2 = conn.open_websocket("wss://example.com/notifications", None).await?;HTTP/3 WebSocket runs over QUIC, providing benefits like 0-RTT, no head-of-line blocking, and better mobile performance.
use sockudo_ws::{WebSocketServer, Http3, Config, Message};
use futures_util::{SinkExt, StreamExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load TLS certificates (required for QUIC)
let tls_config = load_server_tls_config()?;
let ws_config = Config::builder()
.http3_idle_timeout(30_000)
.build();
let server = WebSocketServer::<Http3>::bind(
"0.0.0.0:4433".parse()?,
tls_config,
ws_config,
).await?;
println!("HTTP/3 server listening on {}", server.local_addr()?);
server.serve(|mut ws, req| async move {
println!("HTTP/3 WebSocket at: {}", req.path);
// Same API as HTTP/1.1 and HTTP/2!
while let Some(msg) = ws.next().await {
if let Ok(msg) = msg {
ws.send(msg).await.ok();
}
}
}).await?;
Ok(())
}| Feature | Benefit |
|---|---|
| No head-of-line blocking | One slow stream doesn't block others |
| 0-RTT connection resumption | Faster reconnections |
| Better mobile performance | Handles network changes gracefully |
| Multiple streams per connection | Efficient multiplexing |
io_uring provides kernel-level async I/O with zero-copy operations. It's a transport layer that can be combined with any protocol.
use sockudo_ws::io_uring::UringStream;
use sockudo_ws::{Config, WebSocketStream};
#[tokio_uring::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = tokio_uring::net::TcpListener::bind("127.0.0.1:8080".parse()?)?;
loop {
let (tcp_stream, _) = listener.accept().await?;
// Wrap in UringStream for io_uring I/O
let uring_stream = UringStream::new(tcp_stream);
tokio_uring::spawn(async move {
let mut ws = WebSocketStream::server(uring_stream, Config::default());
while let Some(msg) = ws.next().await {
if let Ok(msg) = msg {
ws.send(msg).await.ok();
}
}
});
}
}Combine io_uring transport with HTTP/2 protocol for maximum performance:
use sockudo_ws::io_uring::UringStream;
use sockudo_ws::{WebSocketServer, Http2, Config};
#[tokio_uring::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = tokio_uring::net::TcpListener::bind("127.0.0.1:8080".parse()?)?;
let server = WebSocketServer::<Http2>::new(Config::default());
loop {
let (tcp_stream, _) = listener.accept().await?;
// 1. Wrap TCP in UringStream for io_uring I/O
let uring_stream = UringStream::new(tcp_stream);
// 2. Add TLS (required for HTTP/2)
// let tls_stream = tls_acceptor.accept(uring_stream).await?;
// 3. Run HTTP/2 WebSocket over io_uring transport
let server = server.clone();
tokio_uring::spawn(async move {
server.serve(uring_stream, |mut ws, req| async move {
// WebSocket over HTTP/2 over io_uring!
while let Some(msg) = ws.next().await {
if let Ok(msg) = msg {
ws.send(msg).await.ok();
}
}
}).await.ok();
});
}
}┌─────────────────────────────┐
│ WebSocket Messages │ ← Your application code
├─────────────────────────────┤
│ WebSocketStream<H2Stream> │ ← sockudo-ws
├─────────────────────────────┤
│ HTTP/2 (h2 crate) │ ← Extended CONNECT framing
├─────────────────────────────┤
│ TLS (rustls/openssl) │ ← Required for HTTP/2
├─────────────────────────────┤
│ UringStream │ ← io_uring async I/O
├─────────────────────────────┤
│ TCP (kernel) │ ← io_uring submission queue
└─────────────────────────────┘
All transports use the same WebSocketStream<S> API:
// HTTP/1.1 (default)
let ws = WebSocketStream::server(tcp_stream, config);
// HTTP/2
let ws = WebSocketStream::server(h2_stream, config);
// HTTP/3
let ws = WebSocketStream::server(h3_stream, config);
// io_uring
let ws = WebSocketStream::server(uring_stream, config);
// Same message loop for all!
while let Some(msg) = ws.next().await {
ws.send(msg?).await?;
}use sockudo_ws::{Config, Compression};
let config = Config::builder()
.compression(Compression::Shared) // SHARED_COMPRESSOR
.max_payload_length(16 * 1024) // 16KB max message
.ping_interval(30) // Ping after 30s inbound inactivity
.pong_timeout(10) // Matching Pong deadline
.pong_timeout_close(4201, "Pong reply not received in time")
.idle_timeout(0) // Independent hard idle limit disabled
.close_timeout(5) // Bounded Close flush/shutdown
.max_backpressure(1024 * 1024) // 1MB backpressure limit
.build();
// Or use uWebSockets-style defaults
let config = Config::uws_defaults();let config = Config::builder()
.http2_window_size(1024 * 1024) // 1MB stream window
.http2_connection_window_size(2 * 1024 * 1024) // 2MB connection window
.http2_max_streams(100) // Max concurrent streams
.build();let config = Config::builder()
.http3_idle_timeout(30_000) // 30 second idle timeout
.build();| Option | Default | Description |
|---|---|---|
compression |
Disabled |
Compression mode |
max_message_size |
64MB | Maximum message size |
max_frame_size |
16MB | Maximum single frame size |
idle_timeout |
120s | Hard inbound-idle deadline, independent of Pong detection (0 = disabled) |
max_backpressure |
1MB | Max write buffer before dropping connection |
auto_ping |
true | Enable proactive native Ping; automatic Pong/Close responses remain enabled when false |
ping_interval |
30s | Inbound inactivity before one native Ping (0 = disabled) |
pong_timeout |
10s | Matching Pong deadline after Ping flush (0 = no deadline and no second Ping until a match) |
pong_timeout_close_code |
1001 | Close code for a missed Pong |
pong_timeout_close_reason |
Pong reply not received in time |
Close reason for a missed Pong |
close_timeout |
5s | Bound for timeout Close flush and transport shutdown |
write_buffer_size |
16KB | Cork buffer size |
Keepalive is driven by valid inbound activity, not a fixed cadence. After
ping_interval with no inbound frame, sockudo-ws writes one RFC 6455 Ping with
an opaque 8-byte nonce. The Pong timer begins only after that Ping is written
and flushed. Only a Pong with the exact payload clears it; unsolicited, stale,
late, or wrong-payload Pongs do not.
Any valid non-Pong inbound frame resets inactivity, including while a Ping is
outstanding, but it does not extend or satisfy that Ping's Pong deadline. If a
hard idle_timeout and Pong deadline tie, the more specific Pong timeout wins,
so only one Close and one typed terminal error are produced.
On timeout, the server rejects further application data, attempts one
configured Close within close_timeout, and completes local cleanup even when
the peer is unreachable. Code 4201 can be observed on a writable path, but no
implementation can guarantee delivery through an already-dead TCP path.
These semantics are shared by Tokio and Compio, plain and permessage-deflate, unified and split APIs. Generic TCP/TLS and HTTP/2/HTTP/3 streams inherit the state machine from their runtime WebSocket stream.
| Mode | Description |
|---|---|
Compression::Disabled |
No compression |
Compression::Dedicated |
Per-connection compressor (best ratio, more memory) |
Compression::Shared |
Shared compressor (good for many connections) |
Compression::Shared4KB |
Shared with 4KB sliding window |
Compression::Shared8KB |
Shared with 8KB sliding window |
Compression::Shared16KB |
Shared with 16KB sliding window |
| Feature | Default | Description |
|---|---|---|
simd |
✅ | SIMD acceleration for masking and UTF-8 |
tokio-runtime |
✅ | Tokio async runtime support |
compio-runtime |
❌ | Native Compio runtime support with a portable polling driver |
permessage-deflate |
✅ | Compression support (RFC 7692) |
fastrand |
✅ | Fast PRNG for client mask generation |
| Feature | Description |
|---|---|
avx2 |
Enable AVX2 (256-bit SIMD) |
avx512 |
Enable AVX-512 (512-bit SIMD) |
neon |
Enable ARM NEON |
nightly |
Enable additional SIMD on arm, loongarch64, powerpc, s390x |
| Feature | Description |
|---|---|
native-tls |
TLS via tokio-native-tls |
rustls-webpki-roots |
TLS via tokio-rustls with webpki-roots |
rustls-native-roots |
TLS via tokio-rustls with native root certificates |
rustls-platform-verifier |
TLS via tokio-rustls with platform verifier |
Rustls features do not select a crypto provider. Applications must enable
exactly one provider, such as aws-lc-rs or ring, on their direct rustls
dependency.
At least one SHA-1 implementation is required for the WebSocket handshake:
| Feature | Description |
|---|---|
ring |
SHA-1 via ring (recommended with rustls) |
aws_lc_rs |
SHA-1 via AWS LC |
openssl |
SHA-1 via OpenSSL (recommended with native-tls) |
sha1_smol |
Pure Rust SHA-1 fallback |
For client mask generation:
| Feature | Description |
|---|---|
fastrand |
Fast PRNG (default) |
getrandom |
Cryptographically secure RNG |
rand_rng |
Use rand crate |
Transport features are runtime-neutral. Pair http2 or http3 with either tokio-runtime or compio-runtime.
| Feature | Description |
|---|---|
http2 |
HTTP/2 WebSocket (RFC 8441) |
http3 |
HTTP/3 WebSocket (RFC 9220) |
io-uring |
Linux io_uring support |
all-transports |
All transport features |
| Feature | Description |
|---|---|
mimalloc |
Use mimalloc as global allocator (10-30% throughput improvement) |
| Feature | Description |
|---|---|
axum-integration |
Axum web framework support |
full |
All features enabled |
sockudo-ws uses SIMD acceleration for frame masking and UTF-8 validation:
| Architecture | Instructions | Masking | UTF-8 | Stable | Nightly |
|---|---|---|---|---|---|
| x86_64 | SSE2 | ✅ | ✅ | ✅ | ✅ |
| x86_64 | SSE4.2 | ✅ | ✅ | ✅ | ✅ |
| x86_64 | AVX2 | ✅ | ✅ | ✅ | ✅ |
| x86_64 | AVX-512 | ✅ | ✅ | ✅ | ✅ |
| aarch64 | NEON | ✅ | ✅ | ✅ | ✅ |
| arm | NEON | ✅ | ✅ | ❌ | ✅ |
| loongarch64 | LSX | ✅ | ✅* | ❌ | ✅ |
| loongarch64 | LASX | ✅ | ✅* | ❌ | ✅ |
| powerpc | AltiVec | ✅ | ✅* | ❌ | ✅ |
| powerpc64 | AltiVec | ✅ | ✅* | ❌ | ✅ |
| s390x | z13 vectors | ✅ | ✅* | ❌ | ✅ |
*Custom SIMD UTF-8 validation with ASCII fast-path (requires nightly feature).
UTF-8 validation uses:
- simdutf8 for x86_64 (SSE4.2, AVX2, AVX-512), aarch64 (NEON), arm (NEON), wasm32
- Custom SIMD implementations for LoongArch64, PowerPC, and s390x (with
nightlyfeature)
The main WebSocket type implementing Stream + Sink:
// Create server-side stream
let ws = WebSocketStream::server(tcp_stream, config);
// Create client-side stream
let ws = WebSocketStream::client(tcp_stream, config);
// Send messages
ws.send(Message::text("hello")).await?;
ws.send(Message::binary(bytes)).await?;
// Receive messages
while let Some(msg) = ws.next().await {
// handle msg
}
// Close connection
ws.close(1000, "goodbye").await?;
// Backpressure handling
if ws.is_backpressured() {
// Write buffer is full, consider slowing down
}For concurrent read/write operations with zero mutex contention:
let (reader, writer) = ws.split();
// SplitReader - owns decoding and reports shared terminal state
reader.next().await // Receive message
reader.is_closed() // Check if closed (non-blocking)
// SplitWriter - bounded command handle to the connection writer driver
writer.send(msg).await?;
writer.send_text("hello").await?;
writer.send_binary(bytes).await?;
writer.close(1000, "bye").await?;
writer.is_closed() // Check if closed (non-blocking)
writer.flush().await?; // Flush accepted application writesImplementation Details:
- Uses
tokio::io::split()for OS-level stream splitting - Reader owns
ReadHalf<S>and protocol decoder - One connection-scoped driver exclusively owns
WriteHalf<S>and the encoder - Bounded control/application queues provide backpressure under Ping floods
- Pong and Close responses progress without later application
send()/flush() - Dropping either half cancels the driver; EOF/Close/timeout propagates to both
- No lock is held across an await
pub enum Message {
Text(Bytes), // Zero-copy, UTF-8 validated
Binary(Bytes),
Ping(Bytes),
Pong(Bytes),
Close(Option<CloseReason>),
}
// Create messages
let text_msg = Message::text("hello"); // From &str
let text_msg = Message::text(String::from("hello")); // From String
let binary_msg = Message::binary(vec![1, 2, 3]); // From Vec<u8>
// Access text content
if let Message::Text(bytes) = msg {
let text: &str = msg.as_text().unwrap(); // Returns Option<&str>
}cargo testcargo test --features http2
cargo test --features http3
cargo test --no-default-features --features compio-runtime,http2
cargo test --no-default-features --features compio-runtime,http3
cargo test --features fullThese tests bind real loopback TCP/QUIC endpoints and use the public runtime APIs for HTTP/2 and HTTP/3 WebSocket handshakes.
cargo test --all-features --test e2e_runtime_transports
cargo test --no-default-features --features tokio-runtime,http2,http3 --test e2e_runtime_transports
cargo test --no-default-features --features compio-runtime,http2,http3 --test e2e_runtime_transportsThe bundled Rust Autobahn port runs all 517
WebSocket cases against the sockudo-ws echo server, including compression, with
the full message counts and no case exclusions. Requires Rust 1.88+ and Python 3
for process management.
make -C autobahn testThe command builds both binaries, waits for the server, runs four cases at a
time, and stops the server on completion or failure. Reports and logs are saved
in autobahn/reports/; open index.html for case details. A failing case or close
handshake makes the command fail. Concurrent runs are for conformance checking;
their timings should not be treated as isolated latency measurements.
GitHub Actions runs the same command on pull requests and pushes to master or
main, and requires it to pass before publishing a release to crates.io. The
autobahn-reports artifact retains reports and logs for 14 days, including failed
runs. The Autobahn workflow can also be started manually.
sockudo-ws includes fuzz targets for security testing:
# Install cargo-fuzz
cargo install cargo-fuzz
# Run fuzzing (requires nightly)
cd fuzz
cargo +nightly fuzz run parse_frame
cargo +nightly fuzz run unmask
cargo +nightly fuzz run utf8_validation
cargo +nightly fuzz run protocol| Target | Description |
|---|---|
parse_frame |
WebSocket frame parsing with arbitrary bytes |
unmask |
SIMD masking/unmasking operations |
utf8_validation |
UTF-8 validation consistency with std |
protocol |
Frame encoding/decoding round-trip |
Run the examples:
# Basic echo server
cargo run --example simple_echo
# Split streams (concurrent read/write)
cargo run --example split_echo
# Axum integration
cargo run --example axum_echo
# HTTP/2 WebSocket server
cargo run --example http2_echo --features tokio-runtime,http2
# HTTP/3 WebSocket server
cargo run --example http3_echo --features tokio-runtime,http3sockudo-ws/
├── src/
│ ├── lib.rs # Public API, Config
│ ├── stream/ # WebSocket stream types
│ │ ├── mod.rs
│ │ ├── websocket.rs # WebSocketStream, Split types
│ │ └── transport_stream.rs
│ ├── protocol.rs # WebSocket protocol state machine
│ ├── frame.rs # Frame encoding/decoding
│ ├── handshake.rs # HTTP upgrade handshake
│ ├── simd.rs # SIMD masking (AVX/SSE/NEON/AltiVec/LSX)
│ ├── utf8.rs # SIMD UTF-8 validation
│ ├── cork.rs # Write batching buffer
│ ├── deflate.rs # permessage-deflate compression
│ ├── error.rs # Error types with categorization
│ ├── transport.rs # Transport trait (Http1, Http2, Http3)
│ ├── server.rs # WebSocketServer<T: Transport>
│ ├── client.rs # WebSocketClient<T: Transport>
│ ├── multiplex.rs # MultiplexedConnection
│ ├── extended_connect.rs # Shared Extended CONNECT logic
│ ├── compio.rs # Native Compio HTTP/1.1, HTTP/2, and HTTP/3 runtime support
│ ├── http2/ # HTTP/2 WebSocket (RFC 8441)
│ │ ├── mod.rs
│ │ └── stream.rs # Http2Stream wrapper
│ ├── http3/ # HTTP/3 WebSocket (RFC 9220)
│ │ ├── mod.rs
│ │ └── stream.rs # Http3Stream wrapper
│ └── io_uring/ # Linux io_uring transport
│ ├── mod.rs
│ ├── stream.rs # UringStream wrapper
│ └── buffer.rs # Registered buffer pool
├── fuzz/ # Fuzzing targets
│ └── fuzz_targets/
│ ├── parse_frame.rs
│ ├── unmask.rs
│ ├── utf8_validation.rs
│ └── protocol.rs
├── examples/
│ ├── simple_echo.rs # Basic echo server
│ ├── split_echo.rs # Concurrent read/write
│ ├── axum_echo.rs # Axum integration
│ ├── http2_echo.rs # HTTP/2 WebSocket server
│ └── http3_echo.rs # HTTP/3 WebSocket server
├── autobahn/
│ ├── server.rs # Autobahn test server
│ └── Makefile # Build and test automation
├── tests/
│ └── e2e_runtime_transports.rs # Tokio and Compio HTTP/2 + HTTP/3 loopback tests
└── benches/
└── throughput.rs # Criterion benchmarks
- SIMD Masking: Uses AVX2/AVX-512/SSE2/NEON/AltiVec/LSX to XOR mask frames at 16-64 bytes per cycle
- SIMD UTF-8: Validates UTF-8 text at memory bandwidth speeds via simdutf8
- Zero-Copy: Parses frames directly from receive buffer without copying
- Cork Buffer: Batches small writes into 16KB chunks for fewer syscalls
- Vectored I/O: Uses
writev()to send multiple buffers in single syscall - io_uring: Kernel-level async I/O with submission queue batching
- Alignment-Aware SIMD: Handles unaligned prefix/suffix for optimal memory access
- Optional mimalloc: High-performance allocator for reduced allocation latency
MIT
sockudo-ws incorporates ideas and techniques from several excellent WebSocket libraries:
-
uWebSockets - The industry standard for high-performance WebSockets. Inspired the cork/batch writing strategy and overall performance-first design philosophy.
-
tokio-websockets - A well-designed Tokio-native WebSocket library. Borrowed several optimizations including:
- Masked frame fast path for small client frames
- Alignment-aware SIMD implementations
- Multi-architecture SIMD support (LoongArch64 LSX/LASX, PowerPC AltiVec, s390x z13, ARM NEON)
- Feature flag organization (TLS variants, SHA-1 options, RNG options)
- Fuzzing infrastructure
-
fastwebsockets - Deno's high-performance WebSocket library. Referenced for fuzzing patterns and frame parsing optimizations.
-
h2 - HTTP/2 implementation used for RFC 8441 support
-
quinn and h3 - QUIC and HTTP/3 implementations used for RFC 9220 support
-
tokio-uring - io_uring integration for Linux
-
simdutf8 - Battle-tested SIMD UTF-8 validation (used by simd-json, polars, arrow)