Server-Sent Events in Rust: Sending Real-Time Data Without WebSocket

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

When WebSocket Is Too Much, or Why SSE Is Back in Fashion

\n\n

Imagine: you need to send data from the server to the client in real time. Currency rates, notifications about new messages, server logs, file processing progress. The first thing that comes to mind is WebSocket. But what if you don't need a bidirectional channel? What if the client only listens, and the server only speaks?

\n\n

This is where Server-Sent Events (SSE) come into play — a protocol that allows the server to send data to the client over a regular HTTP connection. Without extra handshakes, without complex protocols, without headaches with proxies and load balancers. And yes, on Rust it works absolutely beautifully.

\n\n

In this article, we'll break down how to set up SSE in Rust from scratch using popular frameworks. You'll see working examples, learn about pitfalls, and understand when SSE is your best choice.

\n\n

What Are Server-Sent Events and Why Rust?

\n\n

SSE is a W3C standard (EventSource API) that allows the server to send streaming data to the client over HTTP. The client connects to an endpoint, and the server starts sending messages in the text/event-stream format. Everything works over regular HTTP/1.1 or HTTP/2, requires no special proxies, and is supported by all modern browsers.

\n\n

Why Rust? Because Rust gives us:

\n
    \n
  • Asynchrony without surprises — Tokio and async-std allow you to hold thousands of SSE connections on a single thread
  • \n
  • Memory safety — no leaks, no race conditions in multi-threaded streaming
  • \n
  • Minimal resource consumption — a single SSE connection consumes a few kilobytes
  • \n
  • Ecosystem — Actix Web, Rocket, Axum — all support SSE out of the box or with minimal extensions
  • \n
\n\n

How SSE Works: Breaking Down the Protocol

\n\n

The client creates an EventSource in the browser, specifying a URL. The server responds with the header Content-Type: text/event-stream and starts sending lines in the format:

\n\n
data: Hello, world!\\n\\n
\n\n

Each message is a block where data: contains the payload. Messages are separated by two newline characters. You can transmit JSON, numbers, strings — anything. Additionally, there are id, event, and retry fields.

\n\n

Here's a minimal SSE message example:

\n\n
id: 1\nevent: update\ndata: {"status": "processing", "progress": 42}\n\n
\n\n

The JavaScript client receives this as an update event with a data field containing JSON.

\n\n

Writing an SSE Server in Rust with Actix Web

\n\n

Let's start with the most popular framework — Actix Web. Install the dependencies:

\n\n
[dependencies]\nactix-web = "4"\nactix-rt = "2"\ntokio = { version = "1", features = ["full"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"
\n\n

Now let's write a simple endpoint that will send the current time every 2 seconds:

\n\n
use actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, middleware};\nuse actix_web::rt::time;\nuse futures::stream::StreamExt;\nuse std::time::Duration;\n\n#[get("/events")]\nasync fn stream_events(req: HttpRequest) -> HttpResponse {\n    let mut rx = tokio::sync::broadcast::channel::<String>(100).1;\n\n    let stream = async_stream::stream! {\n        loop {\n            let message = format!("data: {}\\n\\n", chrono::Local::now());\n            yield Ok::<_, actix_web::Error>(\n                web::Bytes::from(message)\n            );\n            time::sleep(Duration::from_secs(2)).await;\n        }\n    };\n\n    HttpResponse::Ok()\n        .insert_header(("Content-Type", "text/event-stream"))\n        .insert_header(("Cache-Control", "no-cache"))\n        .insert_header(("Connection"                

Blogs

Book Recommendations