tides_screen: The Rust repo that pretends to stream video and actually stress-tests WebSockets
A tiny binary, a semaphore, and 10,000 queued tasks. This is a clean look at how Rust can simulate load without losing control of it.
- tides_screen is not really a streaming client, it is a semaphore-gated load harness that keeps pressure bounded.
- The repo's main trick is to queue 10,000 tasks while allowing only a small number of WebSocket sessions to run at once.
- Its small `WSClient` and `WSMessage` boundary keeps transport details from leaking into the rest of the program.
- The code is unfinished, but that roughness makes the concurrency pattern easier to study than a polished product would.
The repo looks like a player. The code acts like a pressure test.
On the surface, tides_screen reads like a streaming tool. The name points one way, but the code points somewhere more useful: straight at bounded concurrency.
There is no public launch thread, no interview trail, and no verified quote to lean on here. That thin public footprint is actually helpful, because the code becomes the only honest narrator.
And the code is blunt. It spawns 10,000 tasks, then uses a semaphore to keep only a small number active at once. That is the whole move, and it is what makes the repo interesting.
The real product is bounded fan-out
The heart of `main.rs` is `Arc
// simplified from src/main.rs
let permits = Arc::new(Semaphore::new(25));
for _ in 0..10_000 {
let permits = Arc::clone(&permits);
task::spawn(async move {
let _permit = permits.acquire().await.unwrap();
let _client = connect_to_websocket().await.unwrap();
});
}
That pattern is easy to miss if you only look at the task count. Ten thousand spawned futures sound like chaos, but the semaphore turns them into a queue with a fixed service rate. The machine stays calm because the limit is explicit.
This is the difference between simulating scale and actually unleashing it. The repo wants backlog, not collapse.
The WebSocket layer stays small on purpose
The networking side is split into a tiny wrapper and a tiny protocol type. `WSClient` hides `WebSocketStream
That boundary matters. Once the rest of the program speaks in `WSMessage`, the transport layer can change without forcing every caller to learn tungstenite's shape.
// simplified from src/websocket/message.rs
pub enum WSMessage {
Text(String),
Binary(Vec<u8>),
Ping(Vec<u8>),
Pong(Vec<u8>),
Close,
}
impl From<Message> for WSMessage {
fn from(msg: Message) -> Self {
match msg {
Message::Text(text) => WSMessage::Text(text),
Message::Binary(bytes) => WSMessage::Binary(bytes),
Message::Ping(bytes) => WSMessage::Ping(bytes),
Message::Pong(bytes) => WSMessage::Pong(bytes),
Message::Close(_) => WSMessage::Close,
Message::Frame(_) => panic!("unexpected frame"),
}
}
}
That is not a fancy abstraction. It is a clean one. The repo only needs a narrow slice of WebSocket behavior, so it keeps the slice narrow.
The unfinished edges are part of the lesson
The repo is plainly experimental. The localhost endpoint is hardcoded, the behavior is sparse, and the versioning reads like a skeleton rather than a finished product.
That weakness is also the value. Because the app is not buried under features, the concurrency pattern is easy to see in isolation.
| Model | What happens | Strength | Failure mode | Best use case |
|---|---|---|---|---|
| Sequential client | One connection runs at a time. | Simplest mental model. | Too slow and too artificial for pressure testing. | Smoke tests and basic sanity checks. |
| Unbounded fan-out | Every task starts at once. | Maximum pressure in the shortest time. | Memory, file descriptors, and network stack can collapse. | Tiny synthetic demos. |
| Semaphore-gated fan-out | Many tasks queue, few run at once. | Predictable resource use with a huge backlog. | You can still bottleneck if the permit count is wrong. | Controlled load simulation. |
| Purpose-built load tool | The harness handles metrics, scheduling, and reporting. | Operationally complete and easier to benchmark. | More setup, more surface area, less tiny-code clarity. | Production-grade testing. |
So the real comparison is not fast versus slow. It is controlled pressure versus uncontrolled pressure. `tides_screen` sits in the middle, which is why it is useful as a specimen even if it is not yet a polished product.
What bounded concurrency beats, and what it does not
If you need to push a backend hard without letting the test itself become the problem, this pattern earns its keep. It is better than a naive spawn storm, and it is better than a sequential loop when the point is to create realistic contention.
It is not the right answer if you need dashboards, distributed runners, historical reporting, or a full test plan. For that, use a dedicated load-testing platform. `tides_screen` is smaller than that, and that is why the idea reads so cleanly.





