# 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`](https://github.com/rust-dd/tako/tree/main/examples/websocket)
crate, which serves two endpoints:

* `GET /ws/echo` — reads each client message and echoes it back
* `GET /ws/tick` — pushes a `tick #N` message 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](/docs/getting-started/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:

Source: [examples/websocket/src/main.rs](https://github.com/rust-dd/tako/blob/main/examples/websocket/src/main.rs)

```rust
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`](/docs/routing):
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:

```rust
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:

```rust
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

```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

```bash
cargo run
```

Connect with any WebSocket client. With `websocat`:

```bash
# 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 second
```

In the browser, the standard `WebSocket` API works unchanged:

```js
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](/docs/transports/websocket)
for the defaults.

<Callout type="info">
  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](/docs/transports/websocket).
</Callout>

## 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](/docs/transports/sse) give you a one-way `text/event-stream` from an
ordinary HTTP handler, with browser auto-reconnect built in, behind the `sse`
feature:

```rust
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](/docs/transports/websocket) — the full
  `WebSocketUpgrade` builder and configuration.
* [Server-Sent Events](/docs/transports/sse) — the one-way streaming
  alternative.
* [Building a REST API](/docs/tutorials/rest-api) — the companion tutorial.
* [Deployment](/docs/deployment) — ship the binary.

Source: https://tako.rust-dd.com/docs/tutorials/realtime-chat