Realtime over WebSocket
Build a full-duplex WebSocket feature with an echo handler and a server-push ticker, then compare it to the SSE alternative.
This tutorial builds a realtime feature over a full-duplex WebSocket
connection. It is grounded in the runnable
examples/websocket
crate, which serves two endpoints:
GET /ws/echo— reads each client message and echoes it backGET /ws/tick— pushes atick #Nmessage to the client once per second
These are the two halves of any chat-style feature: receiving messages from a
client, and pushing messages to it on a schedule or from another source.
WebSocket requires the ws feature.
If you have not served a handler before, read the Quickstart first.
1. The full example
A WebSocket endpoint in Tako is an ordinary HTTP handler that returns
WebSocketUpgrade::new(req, handler). The handler receives the upgraded
WebSocket and drives the conversation. This is the whole file:
use std::time::Duration;
use tako::Method;
use tako::responder::Responder;
use tako::types::Request;
use tako::ws::Message;
use tako::ws::WebSocketUpgrade;
pub async fn ws_echo(req: Request) -> impl Responder {
WebSocketUpgrade::new(req, |mut ws| async move {
if ws.send(Message::text("Welcome to Tako WS!")).await.is_err() {
return;
}
while let Some(Ok(message)) = ws.recv().await {
let reply = match message {
Message::Text(text) => Message::text(format!("Echo: {text}")),
Message::Binary(data) => Message::Binary(data),
_ => continue,
};
if ws.send(reply).await.is_err() {
break;
}
}
})
}
pub async fn ws_tick(req: Request) -> impl Responder {
WebSocketUpgrade::new(req, |mut ws| async move {
let mut ticker = tokio::time::interval(Duration::from_secs(1));
let mut tick = 0u64;
loop {
tokio::select! {
message = ws.recv() => match message {
Some(Ok(_)) => {}
_ => break,
},
_ = ticker.tick() => {
if ws.send(Message::text(format!("tick #{tick}"))).await.is_err() {
break;
}
tick += 1;
}
}
}
})
}
#[tokio::main]
async fn main() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.unwrap();
let mut router = tako::router::Router::new();
router.route(Method::GET, "/ws/echo", ws_echo);
router.route(Method::GET, "/ws/tick", ws_tick);
tako::Server::builder()
.build()
.spawn_http(listener, router)
.result()
.await
.expect("HTTP server failed");
}2. How the upgrade works
WebSocketUpgrade::new(req, |ws| async move { … }) is a Responder:
when the handler returns it, Tako answers the WebSocket handshake on the incoming
Request and then runs your closure on the upgraded connection. Inside the
closure, ws.recv() returns the next inbound Message and ws.send() queues an
outbound one. Queued messages go out together once the closure waits for the next
message, so a burst of replies leaves in one write.
The echo handler is a straight read-respond loop:
while let Some(Ok(message)) = ws.recv().await {
let reply = match message {
Message::Text(text) => Message::text(format!("Echo: {text}")),
Message::Binary(data) => Message::Binary(data),
_ => continue,
};
if ws.send(reply).await.is_err() {
break;
}
}Tako answers pings and the closing handshake itself, so the loop only handles
text and binary messages. recv() returns None once the connection has closed.
3. Server-initiated pushes
A chat feed has to push messages the client did not ask for — a new message from
another user, a periodic heartbeat. The ws_tick handler shows the pattern:
tokio::select! races the next inbound message against a timer, so the task both
reacts to the client (stopping when it disconnects) and pushes on its own
schedule:
let mut ticker = tokio::time::interval(Duration::from_secs(1));
let mut tick = 0u64;
loop {
tokio::select! {
message = ws.recv() => match message {
Some(Ok(_)) => {}
_ => break,
},
_ = ticker.tick() => {
if ws.send(Message::text(format!("tick #{tick}"))).await.is_err() {
break;
}
tick += 1;
}
}
}recv() is cancel-safe: when the timer wins, no inbound data is lost. To turn
this into a real chat fan-out, replace the ticker arm with a
tokio::sync::broadcast::Receiver: every connected socket subscribes to a shared
broadcast::Sender, and posting a message sends it to every subscriber. The
select! skeleton stays identical — one arm reads the client, the other reads
the broadcast channel.
4. The Cargo.toml
[dependencies]
tako-rs = { version = "2.5", features = ["ws"] }
tokio = { version = "1", features = ["full"] }The ws feature enables tako::ws, which also provides the Message type, so
no other WebSocket crate is needed.
5. Running it
cargo runConnect with any WebSocket client. With websocat:
# echo
websocat ws://127.0.0.1:8080/ws/echo
# type a line; the server replies "Echo: <line>"
# server push
websocat ws://127.0.0.1:8080/ws/tick
# prints "tick #0", "tick #1", … once per secondIn the browser, the standard WebSocket API works unchanged:
const ws = new WebSocket("ws://127.0.0.1:8080/ws/echo");
ws.onmessage = (e) => console.log(e.data);
ws.onopen = () => ws.send("hello");Tuning the connection
WebSocketUpgrade is a builder. Before returning it you can constrain the
handshake and the frames — protocols, allowed_origins, max_frame_size,
max_message_size, read_buffer_size, write_buffer_size, upgrade_timeout
and max_lifetime. These guard against oversized frames and stalled upgrades on
a public endpoint. See the WebSocket transport reference
for the defaults.
The same handler runs on Compio when you enable compio next to ws.
HTTP/2 WebSocket (RFC 8441) and the deflate extension are not supported; see the
transport reference.
When to use SSE instead
If the data only flows server → client — a live ticker, a log tail, a
progress feed — a WebSocket is more machinery than you need. Server-Sent
Events give you a one-way text/event-stream from an
ordinary HTTP handler, with browser auto-reconnect built in, behind the sse
feature:
use futures_util::stream;
use tako::responder::Responder;
use tako::sse::{Sse, SseEvent};
use tako::types::Request;
async fn feed(_: Request) -> impl Responder {
let events = stream::iter([
SseEvent::data("user joined").event("presence"),
SseEvent::data("hello").event("message"),
]);
Sse::events(events)
}Reach for a WebSocket when the client also needs to send — chat input, collaborative editing, game state. Reach for SSE when it only needs to listen.
Next steps
- WebSocket transport — the full
WebSocketUpgradebuilder and configuration. - Server-Sent Events — the one-way streaming alternative.
- Building a REST API — the companion tutorial.
- Deployment — ship the binary.
Last updated on