Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
173: Server-Sent Events and Streaming Responses in Axum
Server-Sent Events (SSE) are a fantastic way to push updates from your server to a browser without the overhead of WebSockets. I often reach for SSE when I need a one-way stream—like a live log viewer, a stock ticker, or a notification feed—because it's just HTTP. It's simpler to implement and plays nicer with load balancers.
The "Trait Bound" Wall
When I first started using Sse in Axum, I tried to be "efficient" by streaming raw strings. I figured the browser just wanted text, so why bother with extra wrappers? Here is the kind of code that usually leads to a wall of compiler errors:
use axum::{response::Sse, routing::get, Router};
use tokio_stream::Stream;
use std::time::Duration;
async fn stream_updates() -> Sse> {
let stream = tokio_stream::iter(vec!["Update 1".to_string(), "Update 2".to_string()]);
Sse::new(stream)
}
// This will fail to compile!
If you try to run this, the compiler will scream at you. It'll tell you that String doesn't implement the necessary traits to be an SSE event. The mistake here is assuming that Sse is just a wrapper for any stream of text. In reality, the SSE protocol requires a specific format (e.g., data: my message\n\n), and Axum enforces this by requiring the stream to yield SseEvent objects, wrapped in a Result.
Converting Raw Data into SseEvents
To fix this, we need to change the stream's item type. Axum's Sse expects a stream of Result<SseEvent, E>. Since we usually aren't expecting our stream to fail in a way that requires a complex error type, we use Infallible.
Here is the corrected version. Notice how I've used SseEvent::default() to wrap the string. This tells Axum to format the output as a standard "message" event:
use axum::{response::Sse, routing::get, Router};
use tokio_stream::Stream;
use std::convert::Infallible;
use axum::response::sse::SseEvent;
use tokio_stream::StreamExt;
async fn stream_updates() -> Sse<impl Stream<Item = Result<SseEvent, Infallible>>> {
let stream = tokio_stream::iter(vec!["Update 1", "Update 2", "Update 3"])
.map(|msg| Ok(SseEvent::default().data(msg)));
Sse::new(stream)
}
I prefer using .map() on the stream itself. It keeps the logic clean and ensures that every piece of data hitting the wire is correctly formatted for the browser's EventSource API. If you need to send specific event names (like "heartbeat" or "alert"), you can use SseEvent::event("alert").data("System Overload!") instead of the default.
Handling Long-Lived Streams with Channels
In a real app, you aren't usually streaming a static Vec. You're likely streaming events from a database, a message queue, or another part of your system. This is where tokio::sync::mpsc channels come in.
The trick here is that the Receiver half of a channel isn't a Stream by default. You'll need the ReceiverStream wrapper from the tokio-stream crate. I've found that managing the "broadcast" aspect is the hardest part—if you want multiple users to see the same stream, a simple MPSC channel won't work because it's "multi-producer, single-consumer." For that, you'd want tokio::sync::broadcast.
Here is a pattern for a basic event broadcaster. I'm using a broadcast channel so that every connected client gets the same updates:
use axum::{response::Sse, response::sse::SseEvent, routing::get, Router};
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt};
use std::{convert::Infallible, time::Duration};
struct AppState {
tx: broadcast::Sender<String>,
}
async fn sse_handler(
axum::extract::State(state): axum::extract::State<std::sync::Arc<AppState>>,
) -> Sse<impl tokio_stream::Stream<Item = Result<SseEvent, Infallible>>> {
// Subscribe to the broadcast channel
let rx = state.tx.subscribe();
// Wrap the receiver in a BroadcastStream to make it a Stream
let stream = BroadcastStream::new(rx)
.filter_map(|res| res.ok()) // Ignore lag errors
.map(|msg| Ok(SseEvent::default().data(msg)));
Sse::new(stream)
}
#[tokio::main]
async fn main() {
let (tx, _) = broadcast::channel(100);
let state = std::sync::Arc::new(AppState { tx: tx.clone() });
// Simulate a background task pushing updates
let tx_clone = tx.clone();
tokio::spawn(async move {
let mut count = 0;
loop {
let _ = tx_clone.send(format!("Server time: {}", count));
count += 1;
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
let app = Router::new().route("/events", get(sse_handler)).with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
One final tip: always keep an eye on your channel capacity. If your clients are slow and your buffer fills up, broadcast will start lagging. That's why I used .filter_map(|res| res.ok())—it silently drops messages for clients that can't keep up, preventing one slow browser tab from crashing your entire event pipeline.
📋 Practical Task
Build a Real-time System Resource Monitor
Your goal is to create an Axum server that streams simulated system metrics (CPU and Memory usage) to a client using SSE.
- Create a
ResourceMetricstruct that holds acpu_usage(f64) andmem_usage(f64). - Implement a background task that generates random values for these metrics every 1 second.
- Use a
tokio::sync::broadcastchannel to send these metrics to all connected clients. - The SSE handler must serialize the
ResourceMetricstruct into a JSON string before wrapping it in anSseEvent. - Ensure the stream handles
broadcast::error::RecvErrorgracefully so that the connection doesn't drop just because a client lagged.
There are no comments for now.