In this lesson, you will learn how to architect a topic-based message distributor in Rust capable of handling millions of concurrent asynchronous subscribers without exhausting system resources.
The Challenge of Scale
When building a Pub-Sub system, the naive approach—maintaining a Vec<Sender<T>> for every topic—fails quickly. If a topic has a million subscribers, iterating through that list to clone a message is a CPU-intensive O(N) operation that blocks your executor. Furthermore, holding millions of active mpsc channels consumes significant memory for buffer overhead.
The Broadcast Architecture
To scale, we decouple the message production from the subscriber delivery using an async broadcast pattern. Instead of direct channel communication, we use a central "Topic Manager" that leverages tokio::sync::broadcast or an external event bus.
The core idea is to represent subscribers as lightweight streams rather than explicit channel endpoints. By using Arc<Topic> to manage shared state, we can broadcast messages efficiently:
use tokio::sync::broadcast;
use std::sync::Arc;
struct Topic {
tx: broadcast::Sender<Arc<String>>,
}
async fn publish(topic: &Topic, msg: String) {
let _ = topic.tx.send(Arc::new(msg));
}
async fn subscribe(topic: &Topic) {
let mut rx = topic.tx.subscribe();
while let Ok(msg) = rx.recv().await {
// Process message for this subscriber
println!("Received: {}", msg);
}
}
Optimizing for Millions
To prevent one slow subscriber from lagging the entire system, we utilize broadcast::Receiver's lag detection. If a subscriber falls behind, we explicitly handle the lag by skipping messages or dropping the connection, ensuring that the "slowest consumer" doesn't force the publisher to buffer unbounded amounts of data.
When scaling to millions, we shard the TopicManager across multiple async tasks. This prevents a single Mutex or RwLock from becoming a bottleneck when subscribers join or leave. By partitioning topics across different threads, we ensure that the task of routing a message remains non-blocking and latency-sensitive.
Key Takeaway: Scaling a Pub-Sub system relies on minimizing per-subscriber memory overhead and using non-blocking broadcast primitives that allow slow consumers to gracefully drop messages rather than blocking the global message flow.