Remote channels
MPSC, oneshot, watch and broadcast, with the API you already know from Tokio. They all share one transport and messages are sent in chunks, so a big blob on one channel does not stall the others.
Rust RPC with remote channels and objects over one connection.
Remoc turns Rust traits marked with
#[rtc::remote]
into remotely callable interfaces, generating their clients and servers. Unlike
conventional RPC, arguments and return values can contain
Tokio-style remote channels
and remote objects that remain usable after the call.
A channel endpoint is an ordinary value that can travel inside a message, creating a new communication path wherever it arrives, without opening a port, looking up a name or registering anything. RPC calls, channels and remote objects are multiplexed over one TCP, TLS, QUIC, WebSocket, pipe or other transport connection, each with independent flow control.
A single transport connection carries any number of independent, typed channels in either direction.
Channel endpoints can be sent inside messages, creating new communication paths without another transport connection. Here, the client asks the server to count and sends a channel for returning the numbers.
use remoc::prelude::*;
#[derive(Serialize, Deserialize)]
struct CountReq {
up_to: u32,
// A sender, on its way to the peer.
seq_tx: rch::mpsc::Sender<u32>,
}
while let Some(req) = rx.recv().await? {
for i in 0..req.up_to {
req.seq_tx.send(i).await?;
}
}
let (seq_tx, mut seq_rx) = rch::mpsc::channel(1);
tx.send(CountReq { up_to: 4, seq_tx }).await?;
// The channel is live now, nothing to set up.
while let Some(i) = seq_rx.recv().await? {
println!("{i}");
}
A call that hands out another object normally costs a round trip before that
object can be used. Mark the method #[pipelinable] and the caller
creates the client itself, then calls it while the request that opens it is
still on its way.
#[rtc::remote]
trait Directory {
// Existing callers remain unchanged.
#[pipelinable]
async fn open_counter(&self, name: String) -> Result<CounterClient, OpenError>;
}
// Request counter client, one round trip.
let mut counter = dir.open_counter(name).await?;
// Two calls, two round trips.
counter.increase(20).await?;
counter.increase(45).await?;
// Final call, one round trip.
let value = counter.value().await?;
// Total: four round trips.
// Create the counter client up front, no round trip.
let (mut counter, counter_rx) = CounterClient::new();
// Four calls, one round trip.
let value = rtc::calls!(
dir.open_counter_pipelined(name, counter_rx);
counter.increase_call(20);
counter.increase_call(45);
counter.value_call()
);
// Total: one round trip.
A pipelined call may hand out a further object in turn, so a chain of them is
still one round trip.
How
pipelining works →
Remoc uses Postbag to encode your data, allowing independently deployed versions to keep communicating as their message types change. Fields and enum variants can be added, removed, renamed or reordered, while recoverable fields can isolate an incompatible change instead of losing the entire enclosing message.
MPSC, oneshot, watch and broadcast, with the API you already know from Tokio. They all share one transport and messages are sent in chunks, so a big blob on one channel does not stall the others.
Putting #[rtc::remote] on a trait generates client and server
implementations. Arguments and return values may contain channels, allowing a
method to return a stream of updates instead of a single value.
Calls can be traced across both endpoints with OpenTelemetry.
Send a closure over and let the other side call it, for example to report progress or to be told when something happens. The arguments travel one way, the result comes back.
Hash maps, B-tree maps, vectors and sets that publish their changes. A subscriber receives an initial snapshot followed by each change as it occurs.
Locks and read/write locks accessible from another machine, plus lazy values that are fetched only when accessed.
Remoc is transport-independent. It operates over an AsyncRead and
AsyncWrite pair or a Sink and Stream of
packets. Worked examples cover TCP, TLS, QUIC, WebSockets and pipes.
Each channel, remote function and trait call has independent flow control. A receiver that stops reading stalls only its own sender. Buffering remains bounded without per-call configuration.
In the published benchmarks, Remoc remains within 2 % of a plain TCP implementation in most tested configurations and saturates a 1 Gbit/s link.
Remoc contains no unsafe code and is built on Tokio. It also targets
wasm32-unknown-unknown and WASI,
allowing a browser tab to be one endpoint of a connection.
See where Remoc, tarpc, gRPC and Cap'n Proto differ in their contracts, transferable capabilities, conversation patterns and deployment trade-offs.
| Capability | Remoc | tarpc | gRPC | Cap'n |
|---|---|---|---|---|
| Rust trait contract | yes | yes | no | no |
| Typed channels as values | yes | no | no | no |
| Promise pipelining | yes | no | no | yes |
| Either peer can serve | yes | no | no | yes |
Remoc sessions are stateful: channels and remote objects remain connected for as long as the session does. The Aggligator crate provides a transport that keeps Remoc's logical connection alive when underlying links fail, reconnecting automatically without requiring the session to start over. It can also combine the bandwidth of multiple links.
More on channels, RPC and inter-process communication (IPC) across process and machine boundaries.
How channel endpoints work across process boundaries and what happens when the connection is lost.
How any number of channels share a single TCP connection without one of them being able to stall the others.
Sending a channel to the other side so that a service can push events and progress instead of only answering requests.
A daemon and its client over a UNIX socket or a pipe, with Rust traits as the interface instead of a hand-rolled protocol.
A comparison of Remoc's channels and remote objects with Erlang processes and OTP.