Kafka semantics. Merkle integrity. Zero infrastructure.
An embedded event log for Rust.
Overview
No JVM, no ZooKeeper, no network, no containers. Add a dependency, open a directory. Your event log starts when your process starts.
Every partition is a merkle tree. Every record gets a SHA-256 inclusion proof. Prove any record hasn't been modified—the math is the trust model.
Atomic writes (temp+fsync+rename) for all metadata. Index fsynced on every write. Jepsen-style fault injection proves recovery from crashes and truncation.
Topics, partitions, consumer groups, offset management. Subscribe, poll, commit, close—the lifecycle you already know.
RwLock per partition, RwLock on topic map, Mutex per consumer group. Readers never block readers. Writers only block their own partition.
All state survives restarts. Optional LZ4 compression. Configurable retention with max_records per topic. Batch API amortises fsync.
Benchmarks
Single-threaded, single-partition, uncompressed. Batch writes amortise fsync.
See BENCHMARKS.md for full results.
Applications
Regulatory environments (SOX, HIPAA, PCI-DSS, GDPR) require demonstrating that audit records haven't been modified after the fact. merkql provides a cryptographic inclusion proof for every record—hand an auditor a proof and a root hash and they can independently verify authenticity without access to your systems.
// Write an audit event
let producer = Broker::producer(&broker);
producer.send(&ProducerRecord::new("audit",
Some("user-1001".into()),
r#"{"action":"access_patient_record","id":"R-2847"}"#
)).unwrap();
// Later: generate a proof for any record
let topic = broker.topic("audit").unwrap();
let partition = topic.partition(0).unwrap().read().unwrap();
let proof = partition.proof(0).unwrap().unwrap();
// Anyone can verify independently
assert!(MerkleTree::verify_proof(&proof, partition.store()).unwrap());
Event-sourced applications need an append-only log with multiple consumer groups, offset tracking, retention, and durability. merkql provides all of these as a library—no Docker, no cluster management, no network configuration.
let config = BrokerConfig {
compression: Compression::Lz4,
default_retention: RetentionConfig { max_records: Some(100_000) },
..BrokerConfig::new("./data/events")
};
let broker = Broker::open(config).unwrap();
// Each projection gets its own consumer group
let mut projections = Broker::consumer(&broker, ConsumerConfig {
group_id: "order-summary-projection".into(),
auto_commit: false,
offset_reset: OffsetReset::Earliest,
});
Replace a real Kafka cluster in your test suite. Same produce/subscribe/poll/commit lifecycle, starts in microseconds from a temp directory, provides the same consumer group semantics—without Docker, port conflicts, or test isolation problems.
#[test]
fn order_processing_pipeline() {
let dir = tempfile::tempdir().unwrap();
let broker = Broker::open(BrokerConfig::new(dir.path())).unwrap();
// Simulate upstream events
let producer = Broker::producer(&broker);
for order in test_orders() {
producer.send(&ProducerRecord::new("orders", None, order)).unwrap();
}
// Run your real consumer logic
let mut consumer = Broker::consumer(&broker, config);
consumer.subscribe(&["orders"]).unwrap();
let records = consumer.poll(Duration::from_millis(100)).unwrap();
assert_eq!(records.len(), test_orders().len());
}
IoT gateways, POS terminals, medical devices, vehicle telemetry—anywhere you need local event buffering with integrity guarantees and no network dependencies. LZ4 compression keeps storage manageable. Retention policies prevent unbounded growth.
Run your Kafka-based microservices locally without Docker Compose. Same topic/partition/consumer-group semantics, zero startup time.
Getting Started
cargo add merkql
use merkql::broker::{Broker, BrokerConfig};
use merkql::consumer::{ConsumerConfig, OffsetReset};
use merkql::record::ProducerRecord;
use std::time::Duration;
let broker = Broker::open(BrokerConfig::new("/tmp/my-log")).unwrap();
// Produce
let producer = Broker::producer(&broker);
producer.send(&ProducerRecord::new(
"events", Some("user-1".into()), r#"{"action":"login"}"#
)).unwrap();
// Consume
let mut consumer = Broker::consumer(&broker, ConsumerConfig {
group_id: "my-service".into(),
auto_commit: false,
offset_reset: OffsetReset::Earliest,
});
consumer.subscribe(&["events"]).unwrap();
let records = consumer.poll(Duration::from_millis(100)).unwrap();
for record in &records {
println!("{}: {}", record.topic, record.value);
}
consumer.commit_sync().unwrap();
consumer.close().unwrap();
let topic = broker.topic("events").unwrap();
let partition = topic.partition(0).unwrap().read().unwrap();
let proof = partition.proof(0).unwrap().unwrap();
use merkql::tree::MerkleTree;
assert!(MerkleTree::verify_proof(&proof, partition.store()).unwrap());
API
If you know Kafka, you already know merkql.
| merkql | Kafka |
|---|---|
Broker::open(config) | bootstrap.servers |
Broker::producer(broker) | new KafkaProducer<>(props) |
producer.send(record) | producer.send(record) |
producer.send_batch(records) | merkql-specific |
Broker::consumer(broker, config) | new KafkaConsumer<>(props) |
consumer.subscribe(&["topic"]) | consumer.subscribe(List.of(...)) |
consumer.poll(timeout) | consumer.poll(Duration) |
consumer.commit_sync() | consumer.commitSync() |
consumer.close() | consumer.close() |
Verification
Jepsen-style test suite: fault injection with real assertions, property-based testing, and data-backed correctness claims at scale.
| Property | Evidence |
|---|---|
| Total order | Offsets monotonically increasing, gap-free across 10K records and 4 partitions |
| Durability | 5,000 records survive 3 broker close/reopen cycles |
| Exactly-once | Consumer groups deliver every record exactly once across 4 commit/restart phases |
| Merkle integrity | 100% valid inclusion proofs for 10,000 records |
| Byte fidelity | 500 edge-case payloads preserved exactly (unicode, CJK, RTL, up to 64KB) |
| Crash recovery | 1,000 records recovered after 10 ungraceful drop cycles |
Every fault test asserts correctness—not just survival.
| Fault | Assertion |
|---|---|
| Crash (drop without close) | All committed records recovered |
| Truncated snapshot | Safe failure or graceful reopen |
| Truncated index | Reopens with N-1 records, zero read errors |
| Missing snapshot | All records readable, new appends succeed |
| Index ahead of snapshot | Records readable, proofs valid |
Context
| merkql | Kafka | SQLite WAL | Custom file | |
|---|---|---|---|---|
| Consumer groups | ✓ | ✓ | — | — |
| Offset tracking | ✓ | ✓ | — | — |
| Partitions | ✓ | ✓ | — | — |
| Tamper detection | Merkle proofs | — | — | — |
| Deployment | Library | Cluster | Library | Library |
| Compression | LZ4 | Multiple | — | Manual |
| Retention | ✓ | ✓ | Manual | Manual |
| Crash safety | Atomic + fsync | Replicated log | WAL | Manual |
| Distributed | — | ✓ | — | — |
Internals
Content-addressed storage with a git-style object layout.
.merkql/
topics/
{topic-name}/
meta.bin # Topic config (partition count)
partitions/
{id}/
objects/ab/cdef... # Content-addressed merkle nodes + records
offsets.idx # Fixed-width index: offset → record hash
tree.snapshot # Incremental tree state (atomic write)
retention.bin # Retention marker (atomic write)
groups/
{group-id}/
offsets.bin # Committed offsets per topic-partition
config.bin # Broker config
Every object is keyed by its SHA-256 hash—identical data is never stored twice. The merkle tree uses an incremental binary carry chain: appends are O(log n) and the tree state is captured in a compact snapshot that survives restarts without replaying the log.
Scope
merkql runs on one machine. If you need fault tolerance across nodes, use Kafka.
merkql is a library, not a server. For remote producers/consumers, put an API in front of it.
Each partition is independent.
Records are opaque byte strings.
merkql is for workloads where the Kafka programming model is right but the Kafka deployment model is wrong.