Channels at a process boundary

Programs get split for good reasons. Work moves to another machine, a privileged part is separated from a user interface, or a component becomes a service of its own. The channels that connected the tasks cannot cross the new boundary, and what used to be a single send now requires a socket, a wire format, framing and dispatch code.

Replacing the channels with a hand-rolled protocol changes the shape of the program: the protocol grows a message enum and match statements on both sides, and every new interaction touches all of them. An RPC framework such as gRPC restructures the code into request and response services, which is a different model than the tasks and channels you started with.

Channels over one connection

Remoc provides MPSC, oneshot, watch and broadcast channels with the API you already know from Tokio. Their endpoints are ordinary Serde values, so you can send a channel over the network inside a message, like you would hand its endpoint to another task. Each channel is typed and all channels between two peers are multiplexed over one connection, which may be TCP, TLS, QUIC, a WebSocket, a UNIX socket or a pipe.

The program keeps its architecture. Tasks still talk through channels; some of those channels now happen to cross a process or machine boundary.

Replacing a Tokio channel

The channel type comes from remoc::rch instead of tokio::sync and the message derives Serde. Sending and receiving stay the same.

Within one process
use tokio::sync::mpsc;

struct Request {
    reply_tx: mpsc::Sender<Item>,
}

let (tx, mut rx) = mpsc::channel(16);
task_tx.send(Request { reply_tx: tx }).await?;

while let Some(item) = rx.recv().await {
    // ...
}
Between two processes
use remoc::prelude::*;

#[derive(Serialize, Deserialize)]
struct Request {
    reply_tx: rch::mpsc::Sender<Item>,
}

let (tx, mut rx) = rch::mpsc::channel(16);
remote_tx.send(Request { reply_tx: tx }).await?;

while let Some(item) = rx.recv().await? {
    // ...
}

Complete channels example → How sendable channels work →

Questions

Can I send a tokio::sync sender or receiver directly?

No. Tokio's channel endpoints are bound to one process and cannot be serialized. Instead you declare the field with the corresponding remoc::rch type, which can travel inside messages. Its usage stays as you know it from Tokio.

What happens when the connection is lost?

Pending and future send and receive calls fail with an error, so both sides notice. Channels do not silently drop data. If you need the connection itself to survive link failures, Aggligator provides a transport that does that.

Does every channel need its own connection?

No. All channels between two peers are multiplexed over one connection. Opening another channel just sends its endpoint inside a message; there is no additional socket or handshake.

Is there backpressure?

Yes. Each channel has its own bounded buffer and flow control, like a Tokio channel with a capacity. A receiver that stops reading only stalls its own sender, and large messages are sent in chunks so they do not block other channels.

Which transports can I use?

Any ordered byte or packet stream, for example TCP, TLS, a QUIC stream, a WebSocket, a UNIX socket, a pipe to a child process or a serial link. Remoc implements no transport itself and runs over the connection you already have.

Related pages

Multiplexing typed channels explains how the channels share one connection, and IPC between Rust processes covers the local case of a daemon and its clients. Erlang-style message passing in Rust compares the model with distributed Erlang. The Remoc overview also covers remote trait calls and observable collections.