rime
Lock-free delta-xDS control plane for Envoy, written in Rust.
rime is a transport-agnostic xDS management server kernel. It tracks Envoy resources
(clusters, listeners, endpoints, routes), maintains a persistent content-addressed hash
index, and delivers incremental delta xDS updates — sending only what changed, not
everything, every time.
The xDS control plane problem has three hard parts: avoiding lock contention on the
hot read path, shipping only the changed resources to each subscriber, and tracking
per-subscriber acknowledgements so stale state never accumulates. rime solves all
three with four small, composable primitives and zero Arc<Mutex<T>>.
Why delta xDS?
Envoy supports two xDS modes: state-of-the-world (SotW) and delta (incremental). Most control plane libraries — including go-control-plane — implement only SotW. When a single pod restarts in a 200-service cluster, SotW rebuilds and transmits all 200 resources to every subscriber. Delta sends one.
N = 200 services, 1 resource changed:
SotW (go-control-plane path) 11,819 ns O(N)
delta_naive 24,141 ns O(N) + hashing overhead
delta_persistent (rime) 757 ns O(1) — index maintained between events
The key insight: a naive delta that re-hashes all N resources to find the one that changed is still O(N). A persistent hash index maintained across events reduces the per-update cost to a single hash and a map lookup — constant time regardless of cluster size.
Crossover point: delta is faster than SotW at N ≥ 25 services. At N = 200 it is 15.6× faster. At N = 1000 the gap is the same: ~760 ns vs O(N) growing without bound.
Architecture
┌─────────────────────────────────────────────────────┐
│ Plane (single writer, &mut self API) │
│ │
│ ┌──────────┐ ┌─────────────────────────────────┐ │
│ │ Index │ │ Snapshot (ArcSwap<ResourceMap>) │ │
│ │ xxh3 │ │ lock-free atomic pointer swap │ │
│ │ HashMap │ │ pub(crate) store, pub load │ │
│ └──────────┘ └─────────────────────────────────┘ │
│ │ │ │
│ └────── apply/remove ──┘ │
│ │ │
│ watch::Sender ──── notifies ─► │
└─────────────────────────────────────────────────────┘
│
┌───────────────▼────────────┐
│ Server (fan-out factory) │
│ │
│ subscribe(rx) → Subscriber │
└─────────────────────────────┘
│
┌─────────────────▼──────────────────┐
│ Subscriber (per-connection state) │
│ │
│ acked: HashMap<String, u64> │
│ initial() → full Update │
│ changed() → incremental Update │
│ acknowledge(name, hash) │
└─────────────────────────────────────┘
Plane is the single writer. &mut self on every mutating method is a
compile-time single-writer guarantee with no runtime cost.
Index is a persistent HashMap<String, u64> mapping resource name → xxh3
hash. It never allocates on the unchanged fast path: get() borrows by &str,
insert() fires only when content actually differs.
Snapshot is an ArcSwap<ResourceMap>. load() is one atomic instruction.
store() is pub(crate) — only Plane can write; external callers receive
Arc<Snapshot> with read-only access.
Subscriber computes its delta from the current snapshot against its own
acked map. Each subscriber tracks independently. No subscriber state leaks to
any other.
Quick start
[]
= "0.1"
= { = "1", = ["sync"] }
= "1"
use ;
use Bytes;
// Plane is the single writer. Server holds the snapshot for readers.
let = new;
let server = new;
// Populate initial state.
plane.apply;
plane.apply;
// Per-connection: subscribe and send the full initial snapshot.
let mut sub = server.subscribe;
let initial = sub.initial;
for in &initial.resources
// When Envoy ACKs a resource, record it so rime won't re-send.
sub.acknowledge;
// Later: a pod restarts. Apply the changed resource.
plane.apply;
// Subscriber wakes only when the snapshot changes, then diffs against acked state.
if let Some = sub.changed.await
// Resource removed from the control plane.
plane.remove;
if let Some = sub.changed.await
Benchmarks
All numbers are Criterion means on an AMD Ryzen 9 (Linux, single-thread).
Raw results are in benches/.
H1 — Lock-free snapshot reads (ArcSwap vs RwLock)
| scenario | ArcSwap | RwLock |
|---|---|---|
| idle (no writer) | 3.8 ns | 4.2 ns |
| burst (writer every 1 ms) | 3.8 ns | 4.3 ns |
ArcSwap is immune to write pressure: load() is one atomic instruction.
RwLock performance degrades under real write contention — these numbers are
single-threaded; contended numbers are considerably worse.
H2 — Zero-copy fan-out (Bytes vs Vec<u8>)
At 100 subscribers, each needing a handle to a 10 KB resource:
| strategy | 100-subscriber fan-out |
|---|---|
Vec<u8> — heap alloc + memcpy per subscriber |
17,006 ns |
Bytes — Arc refcount per subscriber |
880 ns |
| speedup | 19.3× |
Bytes::clone() is a 4.2 ns atomic refcount increment at any payload size.
Vec<u8>::clone() at 10 KB costs 22× more per handle.
H3 — Content-addressed versioning (xxh3 vs alternatives)
At 1 KB (typical xDS resource, e.g., a Cluster or Listener):
| hash | 1 KB latency | notes |
|---|---|---|
| xxh3 | 36.7 ns | chosen for hot path |
| std DefaultHasher | 163.2 ns | — |
| blake3 | 888.7 ns | cryptographic; 24× slower |
Bytes::clone() baseline |
4.2 ns | just an Arc bump |
Content-addressed versioning with xxh3 costs 36.7 ns per 1 KB resource — less
than 1% overhead relative to the serialization cost it replaces. A reconnecting
Envoy instance sends its known hashes; rime sends only resources whose hash
differs. No monotonic integer versions, no version skew.
H4 — Delta update cost vs state-of-the-world rebuild
1 resource changed, rest identical. "persistent" = index maintained across events (the production pattern: only re-hash the resource you know changed).
| services (N) | SotW rebuild | delta naive | delta persistent |
|---|---|---|---|
| 10 | 333 ns | 1,082 ns | 764 ns |
| 25 | 1,538 ns | 2,822 ns | 773 ns |
| 50 | 3,341 ns | 5,814 ns | 763 ns |
| 100 | 6,870 ns | 12,119 ns | 774 ns |
| 150 | 10,526 ns | 18,200 ns | 775 ns |
| 200 | 11,819 ns | 24,141 ns | 757 ns |
Persistent delta is flat at ~760–775 ns regardless of cluster size. SotW grows linearly: crossover is at N ≈ 20 services. At N = 200, persistent delta is 15.6× faster than SotW.
Comparison with go-control-plane
| feature | rime | go-control-plane |
|---|---|---|
| language | Rust | Go |
| snapshot storage | ArcSwap<HashMap> — lock-free |
sync.RWMutex — blocking |
| resource sharing | Bytes — zero-copy Arc refcount |
[]byte — copied per subscriber |
delta xDS (DeltaDiscoveryService) |
yes — per-subscriber diff engine | no — SotW only |
| content-addressed versioning | xxh3 hash, persistent index | monotonic integers |
| per-subscriber ACK tracking | yes — independent per connection | yes |
| transport | bring your own (tonic, h2, …) | gRPC via google.golang.org/grpc |
| single-writer enforcement | &mut self — compile-time |
runtime mutex |
| mutation-tested | yes — 0 survivors | — |
go-control-plane is a mature, production-proven library. rime is the alternative
when you need incremental xDS updates, zero-copy fan-out, and Rust's ownership
model to enforce invariants the compiler can verify.
xDS resource types
rime stores serialized xDS resources as opaque Bytes. It is agnostic to which
of the following types a given name refers to — callers handle serialization:
| abbreviation | full name | typical payload |
|---|---|---|
| LDS | Listener Discovery Service | Listener proto |
| CDS | Cluster Discovery Service | Cluster proto |
| EDS | Endpoint Discovery Service | ClusterLoadAssignment proto |
| RDS | Route Discovery Service | RouteConfiguration proto |
| SDS | Secret Discovery Service | Secret proto |
A typical xDS management server wraps rime::Plane + rime::Server and drives
them from a Kubernetes informer or similar watch mechanism, serializing resources
with prost or tonic before calling plane.apply().
xDS resource types and the DeltaDiscoveryService wire protocol
rime's Subscriber::initial() and Subscriber::changed() map directly onto
the DeltaDiscoveryService wire protocol:
initial()→ populateDeltaDiscoveryResponse.resourcesfor the first responsechanged()→ populate subsequent incrementalDeltaDiscoveryResponse.resourcesUpdate::removed→ populateDeltaDiscoveryResponse.removed_resourcesacknowledge(name, version_info)→ called whenDeltaDiscoveryRequest.response_nonceconfirms receipt
The nonce / version bookkeeping is the caller's responsibility; rime tracks
content hashes, not nonces.
Prior art
- go-control-plane — the
official Envoy xDS library in Go. SotW only; no delta xDS. Uses
sync.RWMutex. - xds-rs — Rust xDS type bindings (generated protobuf). No control plane logic.
- envoy-control — JVM (Kotlin/Spring), SotW.
None of the Rust crates on crates.io implement a delta xDS control plane or a
persistent content-addressed hash index. rime is the first.
License
Licensed under either of MIT or Apache 2.0 at your option.