Realtime: WebSocket & SSE

The framework ships two ways to push data to a connected client, both written from scratch on std with zero runtime dependencies: WebSocket (RFC 6455, bidirectional) and Server-Sent Events (the WHATWG text/event-stream spec, server→client). Both follow the standards to the letter — a browser's native WebSocket and EventSource talk to them directly, no client library needed.

How it works under the hood

A normal handler returns a Response. A realtime handler instead returns a Reply::Upgrade: the server writes the response head, then hands the raw socket to a closure that owns it for the rest of the connection. That closure runs on a dedicated thread, so a long-lived stream never ties up a request-pool worker. TLS still terminates at the edge (nginx/Caddy); the server speaks plain HTTP.

Server-Sent Events

A one-way stream of text events. Ideal for live counters, notifications, logs, progress, and live-reload — anything the server pushes and the client only reads. The browser reconnects automatically.

use akurai_http::{sse, Reply};

fn events() -> Reply {
    sse(|mut sink| {
        let mut n = 0;
        loop {
            n += 1;
            sink.event("tick", &n.to_string())?; // event: tick\ndata: N\n\n
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
    })
}

The SseSink handles the wire format: data, named event, id, retry, and comment (for keep-alive heartbeats). Multi-line data is split into multiple data: lines per the spec, and every send is flushed so events arrive immediately. When the client disconnects the next write fails and the closure returns — the thread ends cleanly.

On the client, it is one line:

const es = new EventSource("/api/events");
es.addEventListener("tick", (e) => console.log("tick", e.data));

The built-in demo endpoint is GET /api/events.

WebSocket

Full-duplex messages over a single connection. Use it for chat, collaborative editing, live dashboards, and anything where the client also sends.

use akurai_ws::{upgrade, Message};

// in a handler returning `akurai_http::Reply`:
upgrade(req, |mut conn| {
    while let Some(msg) = conn.recv()? {
        match msg {
            Message::Text(t) => conn.send_text(&t)?,   // echo
            Message::Binary(b) => conn.send_binary(&b)?,
            Message::Close(_) => break,
            _ => {}
        }
    }
    Ok(())
})

upgrade performs the RFC 6455 handshake — it validates the Upgrade, Connection, Sec-WebSocket-Key, and Sec-WebSocket-Version headers and answers with the SHA-1 + base64 Sec-WebSocket-Accept token, or returns 400 if the request is not a valid WebSocket upgrade.

WsConn does the rest of the protocol for you:

server frames are sent unmasked, per spec.

Text or Binary.

frames close 1009; protocol errors close 1002.

On the client:

const ws = new WebSocket("ws://localhost:8090/api/ws");
ws.onmessage = (e) => console.log("echo:", e.data);
ws.onopen = () => ws.send("hello");

The built-in demo endpoint is GET /api/ws (an echo server).

Choosing between them

it first; most "live" features are one-directional.

Use it when the client must send too.

Limits

Each realtime connection holds one OS thread for its lifetime — the right model at the small/medium scale the framework targets, not for tens of thousands of idle connections. WebSocket negotiates no extensions (no permessage-deflate), and a single frame or reassembled message is capped at 16 MiB to bound memory.