A simplified high-performance low-latency Rust WebSocket client library providing three distinct implementations: async/threaded with channels, non-blocking with callbacks, and blocking with callbacks.
- ⚡ Low latency - Built with Rust as a thin layer over tungstenite-rs and optional for crossbeam-channel
- 🚀 Non-blocking, Blocking, Async Channels - Choose the right approach for your use case
- 🔒 TLS backend - Support for native-tls
- 📡 Event-driven architecture - With either handler callbacks or channels
- 🎯 Type-safe API - Leverage Rust's type system for correctness
- 📊 Built-in tracing - Comprehensive logging support
- 🤖 AI Coding Ready - Support for AI-assisted development with Claude (Vibe Coding)
- 🕑 Predictable latency - Direct system calls and callbacks, means lower baseline latency
- 📋 Low overhead - No task scheduling, futures, or waker infrastructure
- 🤯 Simple model - Straightforward usage
- 📐 Deterministic behavior - Optional custom spin-wait duration gives precise control over CPU/latency tradeoff
- 🚫️ No runtime - No async runtime or framework overhead, no
poll()/epoll_wait()/kevent() - 🔁 Async message passing - Optional message passing through channels
- ⛔️ Graceful Shutdown - Process all messages util connection is closed by server on graceful shutdown
- 📛 Forcibly Shutdown - Break the client's event loop immediately
- Scalability - None of the clients scale to thousands of connections (see Scalability Constraints)
- CPU overhead - The fastest mode
non-blockingwithoutspin-waitutilizes 100% CPU core usage by design
Add this to your Cargo.toml:
[dependencies]
s9_websocket = "0.0.2"The library uses native-tls.
Inspect examples for various usages and use cases.
Choose the client that fits your application architecture:
- S9NonBlockingWebSocketClient - Runs on your thread, receive events via callbacks (lowest latency)
- S9BlockingWebSocketClient - Runs on your thread, receive events via callbacks
- S9AsyncNonBlockingWebSocketClient - Spawns background thread, receive events via channels (easiest usage)
Pure non-blocking client that runs on caller's thread using handler callbacks for events.
Socket Mode: Uses non-blocking socket I/O (set_nonblocking(true)) internally. The name "NonBlocking" refers to the socket I/O mode, not the behavior of run() which blocks the calling thread indefinitely.
Flushing: All send methods flush immediately after write
- Runs entirely on caller's thread
- Receive events through handler callbacks (zero copy, zero allocation)
- Call client methods directly from handler callbacks (send, close, force_quit)
- Lowest possible latency
- Configurable CPU/latency trade-off via spin-wait duration
- Configurable socket options like TCP_NODELAY, TTL, etc
use s9_websocket::{S9NonBlockingWebSocketClient, S9WebSocketClientHandler, NonBlockingOptions};
use std::time::Duration;
// Implement the handler trait
struct MyHandler {
message_count: usize,
}
impl S9WebSocketClientHandler<S9NonBlockingWebSocketClient> for MyHandler {
// Only override the methods you care about
fn on_text_message(&mut self, client: &mut S9NonBlockingWebSocketClient, data: &[u8]) {
let text = String::from_utf8_lossy(data);
println!("Received: {}", text);
self.message_count += 1;
if self.message_count >= 2 {
println!("Closing connection...");
client.close();
} else {
// Send another message
client.send_text_message(&format!("Echo: {}", text)).ok();
}
}
fn on_connection_closed(&mut self, _client: &mut S9NonBlockingWebSocketClient, reason: Option<String>) {
println!("Connection closed: {:?}", reason);
}
fn on_error(&mut self, _client: &mut S9NonBlockingWebSocketClient, error: String) {
eprintln!("Error: {}", error);
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure options
let options = NonBlockingOptions::new()
.spin_wait_duration(Some(Duration::from_millis(10)))?;
// Connect to WebSocket server
let mut client = S9NonBlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;
// Send initial message
client.send_text_message("Hello!")?;
// Create handler
let mut handler = MyHandler { message_count: 0 };
// Run the non-blocking event loop (blocks on this thread)
client.run(&mut handler);
Ok(())
}Synchronous client that runs on caller's thread using handler callbacks for events.
Socket Mode: Uses blocking socket I/O (standard socket reads/writes) internally, with optional timeouts. The name "Blocking" refers to the socket I/O mode, not the behavior of run() which blocks the calling thread indefinitely (same as S9NonBlockingWebSocketClient).
Flushing: All send methods flush immediately after write
- Runs entirely on caller's thread
- Receive events through handler callbacks (zero copy, zero allocation)
- Call client methods directly from handler callbacks (send, close, force_quit)
- Optional read/write timeouts for responsive control message handling (simulates non-blocking behavior)
- Configurable CPU/latency trade-off via spin-wait duration
- Configurable socket options like TCP_NODELAY, TTL, etc
use s9_websocket::{S9BlockingWebSocketClient, S9WebSocketClientHandler, BlockingOptions};
// Implement the handler trait
struct MyHandler {
message_count: usize,
}
impl S9WebSocketClientHandler<S9BlockingWebSocketClient> for MyHandler {
// Only override the methods you care about
fn on_text_message(&mut self, client: &mut S9BlockingWebSocketClient, data: &[u8]) {
let text = String::from_utf8_lossy(data);
println!("Received: {}", text);
self.message_count += 1;
if self.message_count >= 2 {
println!("Closing connection...");
client.close();
} else {
// Send another message
client.send_text_message(&format!("Echo: {}", text)).ok();
}
}
fn on_connection_closed(&mut self, _client: &mut S9BlockingWebSocketClient, reason: Option<String>) {
println!("Connection closed: {:?}", reason);
}
fn on_error(&mut self, _client: &mut S9BlockingWebSocketClient, error: String) {
eprintln!("Error: {}", error);
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure with default blocking behavior
let options = BlockingOptions::new();
// Connect to WebSocket server
let mut client = S9BlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;
// Send initial message
client.send_text_message("Hello!")?;
// Create handler
let mut handler = MyHandler { message_count: 0 };
// Run the blocking event loop (blocks on this thread)
// Handler can call client methods directly from callbacks
client.run(&mut handler);
Ok(())
}Spawns a background thread for socket operations and communicates via channels.
Socket Mode: Uses non-blocking socket I/O (set_nonblocking(true)) internally. The name "NonBlocking" refers to the socket I/O mode, not the behavior of run() which returns immediately after spawning the thread.
Flushing: All send methods flush immediately after write
- Background thread handles all socket operations
- Receive events through built-in channels (
event_rx) - Send commands through built-in channels (
control_tx) - Thread-safe for multi-threaded applications
- Configurable CPU/latency trade-off via spin-wait duration
- Configurable socket options like TCP_NODELAY, TTL, etc
use s9_websocket::{S9AsyncNonBlockingWebSocketClient, WebSocketEvent, ControlMessage, NonBlockingOptions};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure options
let options = NonBlockingOptions::new()
.spin_wait_duration(Some(Duration::from_millis(10)))?;
// Connect to WebSocket server
let mut client = S9AsyncNonBlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;
// Start the event loop (spawns thread)
let _handle = client.run()?;
// Send a message via control channel
client.control_tx.send(ControlMessage::SendText("Hello, WebSocket!".to_string()))?;
// Handle events from channel
loop {
match client.event_rx.recv() {
Ok(WebSocketEvent::Activated) => {
println!("WebSocket connection activated");
},
Ok(WebSocketEvent::TextMessage(data)) => {
let text = String::from_utf8_lossy(&data);
println!("Received: {}", text);
client.control_tx.send(ControlMessage::Close())?;
},
Ok(WebSocketEvent::Quit) => {
println!("Client quit");
break;
},
_ => {}
}
}
Ok(())
}use std::collections::HashMap;
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer token123".to_string());
headers.insert("X-Custom-Header".to_string(), "value".to_string());
let client = S9NonBlockingWebSocketClient::connect_with_headers(
"wss://api.example.com/ws",
&headers
)?;impl S9WebSocketClientHandler<S9NonBlockingWebSocketClient> for MyHandler {
fn on_ping(&mut self, _client: &mut S9NonBlockingWebSocketClient, data: &[u8]) {
println!("Received ping: {:?}", data);
}
fn on_pong(&mut self, _client: &mut S9NonBlockingWebSocketClient, data: &[u8]) {
println!("Received pong: {:?}", data);
}
}use std::time::Duration;
// Maximum performance (no sleep between reads, high CPU usage)
let options = NonBlockingOptions::new(None)?;
// Balanced (10ms sleep between reads)
let options = NonBlockingOptions::new(Some(Duration::from_millis(10)))?;
// Low CPU usage (100ms sleep between reads, higher latency)
let options = NonBlockingOptions::new(Some(Duration::from_millis(100)))?;All clients provide low-level access to the underlying tungstenite WebSocket for advanced use cases:
use s9_websocket::{S9NonBlockingWebSocketClient, NonBlockingOptions};
let options = NonBlockingOptions::new();
let mut client = S9NonBlockingWebSocketClient::connect("wss://echo.websocket.org", options)?;
// Get immutable reference to the socket
let socket = client.get_socket();
// Get mutable reference to the socket for advanced operations
let socket_mut = client.get_socket_mut();Note for S9AsyncNonBlockingWebSocketClient: Socket access returns Option<&WebSocket> because the socket is moved to event loop thread after calling run(). Socket access is only available before run() is called.
Use with caution: Direct manipulation of the underlying socket may interfere with the client's operation.
The library uses the 'tracing' crate for logging. Enable logging in your application: use tracing_subscriber;
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();- Choose the right client:
- Use non-blocking client for low-latency applications
- Use async client for easiest embedding in multithreaded applications
- Tune spin wait duration:
None: Best latency, highest CPU usageSome(Duration::from_millis(1-10)): Good balance for up to 100 message/secSome(Duration::from_millis(50-100)): Lower CPU, higher latency, less throughput
- Tune no_delay:
None: use OS defaultSome(true): socket write operations are immediately executedSome(false): socket write operations are scheduled by OS
None of the clients scale to thousands of connections.
The library's architecture requires one OS thread per connection because each client's run() method either
spawns a dedicated thread (async client) or blocks the caller's thread indefinitely (non-blocking and blocking client).
There is no I/O multiplexing support to run multiple connections on a single thread.
For 1000+ connections, use async/await libraries like tokio-tungstenite or async-tungstenite that provide
true async I/O multiplexing.
Full API documentation is available at https://docs.rs/s9_websocket/latest/s9_websocket.
Contributions are welcome! Please feel free to submit bugs and make feature requests here Further information like coding conventions are currently maintained in CLAUDE.md
This project is licensed under the APACHE / MIT License - see the LICENSE files for details.
Project source code is available at https://github.com/AlexSilver9/s9_websocket.
Built on top of: