Remoc

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.

One connection, many channels

A single transport connection carries any number of independent, typed channels in either direction.

How Remoc uses one connection Two programs, each holding senders and receivers with their own queue of messages: an mpsc sender sending to the right, a watch receiver fed from the right, and an RPC client calling an RPC server. All of them join into a single connection in the middle, which carries their chunks interleaved. Local endpoint Remote endpoint mpsc::Sender mpsc::Receiver watch::Receiver watch::Sender RPC client RPC server chunks of every channel, interleaved one connection TCP, TLS, a WebSocket or anything else that carries bytes
Each channel keeps its own type and its own direction; the connection carries them all, in chunks, so a large message on one does not hold up the others. Back pressure is per channel too: a receiver that stops reading slows only its own sender.

Multiplexing typed channels over one TCP connection →

Channels are cheap, sendable values

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.

How a channel is created by sending one Two frames. In the first, a request travelling over an existing channel between the two endpoints contains a field holding the sender of a new channel. In the second, that channel is live between the endpoints, running inside the same connection as the first one. 1. A request travels over a channel that already exists, carrying the sender of a new one. 2. The new channel is live, inside the same connection. Local endpoint Remote endpoint One connection RPC client or existing mpsc::Sender RPC server or existing mpsc::Receiver count(...) up_to: 4 seq_tx: mpsc::Sender any channel can carry another RPC client or existing mpsc::Sender RPC server or existing mpsc::Receiver mpsc::Receiver mpsc::Sender 0 1 2 3
Any channel can carry another. Here an RPC call hands over the sender of a new mpsc channel; there is no port to open, no name to look up and nothing to register or await.
Shared request
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>,
}
Server
while let Some(req) = rx.recv().await? {
    for i in 0..req.up_to {
        req.seq_tx.send(i).await?;
    }
}
Client
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}");
}

Multiple RPC calls, one round trip

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.

What pipelining saves Two frames, each showing a client timeline above a server timeline with requests travelling down to the server and results back up. In the first, a call on the directory hands out a counter and three further calls are then made on that counter, each waiting for the previous result, so the exchange takes four round trips and fills the width of the picture. In the second, the same four calls leave together and their results come back together, so the exchange takes one round trip and occupies a quarter of the width. Ordinarily: each call waits for the result of the one before it Client Server dir.open_counter("mine") CounterClient counter.increase(20) counter.increase(45) counter.value() counter created 4 round trips Pipelined: every call is sent before any result is awaited Client Server all four requests all four results counter attached, queued calls run 1 round trip
The counter is requested by the caller and calls to be performed with it are sent along with the initial request. They are run the moment the server attaches the counter to it. Only the final request is waited for, so the counter creation and the three calls that depend on it share a single round trip.
The remote trait
#[rtc::remote]
trait Directory {
    // Existing callers remain unchanged.
    #[pipelinable]
    async fn open_counter(&self, name: String) -> Result<CounterClient, OpenError>;
}
Sequential caller
// 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.
Pipelined caller
// 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 →

Messages that evolve with your application

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.

How Postbag handles schema evolution →

Postbag logo: messages entering a satchel surrounded by a gear

Features

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.

rch module →

Remote trait calls (RPC)

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.

rtc module →

Remote callbacks

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.

rfn module →

Observable collections

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.

robs module →

Remote objects

Locks and read/write locks accessible from another machine, plus lazy values that are fetched only when accessed.

robj module →

Transport independence

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.

transports →

Independent back pressure

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.

Transport performance

In the published benchmarks, Remoc remains within 2 % of a plain TCP implementation in most tested configurations and saturates a 1 Gbit/s link.

Benchmarks →

Safe Rust and WebAssembly

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.

Connection resilience with Aggligator

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.

Explore Aggligator →

Aggligator's green crocodile logo

Common uses

More on channels, RPC and inter-process communication (IPC) across process and machine boundaries.