Ursula
Docs: ursula.tonbo.io
Ursula is a self-hosted, distributed server for the replayable, append-only event timelines behind document edits, agent runs, workflows, and chat. It speaks the Durable Streams Protocol over plain HTTP and SSE.
Self-hosted, low-latency, S3-backed, quorum-replicated
Event streams live outside the broker network. Document editors, agents, and durable workflows need timelines that browsers, mobile apps, and serverless functions can read, write, and tail over the public internet. That asks for HTTP-native, distributed, S3-backed infrastructure, not the SDK-locked, single-network shape Kafka-style brokers were built for.
The Durable Streams Protocol nails that wire format, but its reference server is a single process: a node loss is data loss. The other servers we evaluated each force you to give up one of four things this primitive deserves to keep:
- Open-source self-hosting.
- Low write latency (sub-50 ms P99 appends, no batching window required).
- Plain S3 economics (cold tier on standard S3, no S3 Express tier, no per-GB SaaS markup).
- Quorum-replicated durability (acknowledged writes survive a single-node failure).
Ursula keeps all four.
Full design intent: Why Ursula · How Ursula compares.
Thread-per-core, multi-Raft, S3 as cold tier
Three or five Ursula processes act as one durable-streams server. A stream hashes to one Raft group, that group has one replica on each voter node, and the same group ID is owned by a deterministic core on every node. Groups replicate independently; there is no cross-group transaction path.
HTTP / SSE clients
| | |
v v v
+-----------+ +-----------+ +-----------+
| node 1 |<--->| node 2 |<--->| node 3 |
| HTTP/gRPC | | HTTP/gRPC | | HTTP/gRPC |
| | | | | |
| core 0 | | core 0 | | core 0 |
| group 0* |<--->| group 0 |<--->| group 0 |
| group 3 |<--->| group 3* |<--->| group 3 |
| | | | | |
| core 1 | | core 1 | | core 1 |
| group 1 |<--->| group 1* |<--->| group 1 |
| group 4* |<--->| group 4 |<--->| group 4 |
| | | | | |
| core 2 | | core 2 | | core 2 |
| group 2 |<--->| group 2 |<--->| group 2* |
| group 5 |<--->| group 5 |<--->| group 5* |
+-----+-----+ +-----+-----+ +-----+-----+
| | |
+-----------------+-----------------+
| background flush
v
+--------------+
| S3 cold tier |
+--------------+
* leader for that Raft group, leadership can differ per group.
-
Each stream hashes to one Raft group and owner core, so cores own disjoint groups with no shared mutable state on the hot path.
-
Per-group node-to-node Raft.
Every node hosts replicas for the same configured groups, and those replicas exchange gRPC Raft RPCs while non-leader HTTP writes forward to the current group leader.
-
Hot ring on the write path.
Appends commit into an in-memory ring and Raft log while background flushers move older committed chunks to S3.
-
Independent Raft groups.
Each group has its own raft instance, log, state machine, hot ring, watchers, and cold-flush budget, with no cross-group commit protocol.
-
Stateless HTTP front door.
axum parses, routes, and renders the protocol while stream ownership and mutable state stay inside the owning group actor.
Across nodes, writes are leader-serialized within one group and acknowledged after a majority of that group's replicas persist and apply the command. Full design: Architecture overview.
Benchmark
On EC2 (3 × c7g.4xlarge, Raft quorum), Ursula sustains 35.2k appends/sec at 500 streams (5.9× single-node Durable Streams, 5.2× S2 Lite, both on 1 × c7g.4xlarge) and delivers SSE fan-out to 1000 subscribers at 6.1 ms p99 (160× faster than Durable Streams, 18× faster than S2 Lite). Apples-to-apples methodology, full charts, replay and latency cuts: ursula.tonbo.io/benchmark.
Quickstart
For now, Ursula builds from Rust source. Pre-built release binaries are on the way.
Run a single in-memory node (no persistence, good for kicking the tires):
It binds 127.0.0.1:4437, picks a core count from your CPU, and uses an in-memory engine. Override with --listen, --core-count, --raft-group-count, or pick a persistent backend with --wal-dir / --raft-log-dir.
Create a bucket and stream, append bytes, read them back:
Tail the stream live over SSE, new appends arrive as event: data lines immediately:
Walkthroughs: Quick Start · Deploy a cluster · Configure S3.
Roadmap
The v0.1.x line is a working prototype. Next on deck:
-
if-matchconditional append.Optimistic concurrency control on the append path. An
if-match: <offset>header lets a writer commit only when the stream tip hasn't moved, so concurrent writers can coordinate without an external lock. The semantics need to land in Ursula's HTTP adapter and Raft state machine. -
Stateless WASM compute over streams.
A planned Ursula extension: bind a deterministic WASM module to a stream so the server can materialize per-stream state, enabling automatic compaction and
410 Gonebootstrap recovery without application-side checkpointing. -
Dynamic membership.
Online voter / learner reconfiguration and orchestrated rolling membership changes (today's clusters are static).
-
Backup and restore tooling.
A supported recovery path for total-cluster loss from the S3 cold tier (today there is none).
-
Client SDKs.
Ergonomic Rust and TypeScript clients on top of the HTTP API.
Credits
- ElectricSQL for the original Durable Streams Protocol that Ursula implements.
- Loro for the snapshot and replay extension design that Ursula adopted on top of the base protocol.
License
Apache 2.0. See LICENSE.
Built by Tonbo, an open-source storage team.