When channels meet 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.

The usual answers change the shape of your program. A hand-rolled 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.

The same model, over a 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, 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.

What changes in your code

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 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.

Where to go next

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. Building upon the channels, the Remoc overview shows remote trait calls and observable collections.