Ursula: thread-per-core, multi-Raft Rust runtime for HTTP event streams
Summary
Ursula is an open-source, self-hosted distributed server for replayable, append-only event timelines that runs over HTTP and SSE, using a thread-per-core, multi-Raft architecture with S3 storage for low latency and durability.
View Cached Full Text
Cached at: 05/21/26, 04:16 PM
tonbo-io/ursula
Source: https://github.com/tonbo-io/ursula
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.
What Ursula keeps
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.
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):
cargo run --bin ursula
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:
curl -X PUT http://127.0.0.1:4437/demo
curl -X PUT http://127.0.0.1:4437/demo/hello
curl -X POST http://127.0.0.1:4437/demo/hello \
-H 'Content-Type: application/octet-stream' \
--data-binary 'hello world'
curl 'http://127.0.0.1:4437/demo/hello?offset=-1'
Tail the stream live over SSE, new appends arrive as event: data lines immediately:
curl -N 'http://127.0.0.1:4437/demo/hello?offset=-1&live=sse'
Walkthroughs: Quick Start · Deploy a cluster · Configure S3.
Architecture
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.
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.
Similar Articles
I run Claude Code + Codex in parallel and kept losing the thread between them - built a no-server coordination layer that lives in an S3 bucket
tracecraft is a CLI that uses an S3-compatible bucket as a stateless coordination layer for multiple coding agents like Claude Code and Codex, enabling atomic task claims, mailboxes, shared memory, and cross-harness session mirroring.
The Ü Programming Language
Ü is a statically-typed compiled programming language designed for reliability and speed, with safe/unsafe code separation, RAII, and LLVM backend. It aims to be superior to C++ and easier than Rust.
How (and why) we rewrote our production C++ frontend infrastructure in Rust
NearlyFreeSpeech.NET rewrote their production C++ frontend infrastructure (nfsncore) in Rust, a critical system that handles routing, caching, and access control for all incoming requests. The migration was motivated by Rust's safety guarantees, performance, ecosystem strength, and the aging C++ codebase's limitations.
Narwhal v0.6.0 – message broker for edge apps, now with channel persistence
Narwhal 0.6.0 releases with channel persistence, offering a lightweight Rust message broker for edge apps that delegates auth/validation logic to external “modulators.”
Serving files over HTTP three ways: synchronous, epoll, and io_uring
A technical article comparing three approaches to serving files over HTTP: synchronous thread-per-request, epoll-based asynchronous I/O, and io_uring, with code examples in C.