← The Dry Stack

Relay

Pub/sub where the log is the broker — publish is a database append, replay is a memory-speed scan, and delivery is measured in microseconds.

What it is. The messaging layer fused onto Facts: a durable subject is a stream in the column store. Publishing a message and persisting it are the same operation — there is no broker process, no second system for history, no hop between "the queue" and "the database". Subscribers ride live pushes; anyone who falls behind or joins late reads the log directly. Kafka, fused.

The problem it kills. Brokered messaging pays the broker tax twice: every live message crosses to a server and back, and every replay of history is a stream of per-message round trips. Relay's consumers replay history at engine speed — tens of millions of messages a second — because "catching up" is a columnar scan, not a network conversation.

Patterns, not subsystems. Request/reply and streaming responses are a 16-byte convention on top of pub/sub — an ephemeral inbox and a correlation id — not a bolted-on RPC runtime. Across machines, nodes form a full mesh over Nio with credit-based flow control; a consumer that misses messages self-heals from the origin's log, exactly once, in order.

Talk to us ⟶
Measured against NATS · same box, 16-byte messages

NATS is the closest architectural rival — subjects, req/reply, durable streams via JetStream. Same machine, rival-favorable configs (separate pub/sub connections, pipelined publishes, file-backed JetStream with a deep ack window), throughput measured as sustained delivered messages with identical pacing on both sides.

ScenarioRelay · embeddedRelay · mesh¹NATS
Live pub→sub, sustained~6M msg/s2.4M msg/s¹~1.3M msg/s core
Durable pub→sub, sustained (persisted)~4M msg/s~0.15M msg/s JetStream²
Publish→handler latency, p501–3 µs~11 µs
Request/reply round trip, p503–7 µs~20 µs
Replay persisted history~50M msg/s~1.3M msg/s JetStream
Replay at 50M msg/s — ~35× JetStream. History isn't a fetch, it's a scan. A consumer replaying two million persisted messages reads the column store directly — no broker in the read path at all. That is what "the log is the broker" buys.

Read it straight. The embedded column is an architectural comparison, not a wire shootout — Relay lives inside your process, NATS always crosses to a server. That gap is the product. NATS is fast and battle-tested; where it fits, it fits. Relay's bet is that most systems don't need a separate broker at all — and pay dearly for having one.

Test bed: the drylane demo box — bare-metal Ubuntu, 32 threads, .NET 10 Release x64, data on NVMe, nats-server v2.14 (-js, file storage), NATS.Net 2.x, loopback. Ranges are 3 runs on a box that is also serving the live demos, so throughput carries some contention. ¹ Mesh = two RelayNodes over loopback Nio (Windows dev-box figure; box re-measure pending). ² A JetStream publish-ack is a stronger per-message contract than Relay's append-then-periodic-flush — part of that gap is semantics, and we say so. Full harness + methodology in samples/Relay.CompetitiveBench.

What a subscriber gets
See it live

A real two-node cluster, one button. Kill the leader and watch the surviving consumer's delivered counter climb straight through the failover — zero gaps, zero duplicates, the same subscriber that never reconnected.

Break the cluster ⟶
When you should use NATS or Kafka instead

Relay is embedded and .NET-first — it wins by deleting the broker, which also defines where it doesn't fit:

Where it fits in the stack

Zao packs the message, Relay routes and persists it, Facts is the log it lives in, Nio carries it between machines. One stack, no seams — the same bytes from your struct to the wire to the column store.

info@drylane.io ⟶