Brokers are stateful
Every broker owns local JBOD disks, a partition directory tree, and years of partition-reassignment scar tissue. Scaling up means moving terabytes of data between machines.
Kimistore is a lightweight streaming agent that speaks the Apache Kafka binary protocol and keeps your entire log in S3. No ZooKeeper. No KRaft quorum. No replicated broker disks. Point your existing producers and consumers at it and go.
$ kcat -P -b localhost:19092 -t orders
% Reached end of topic orders (Produced 1 new message)
$ kcat -C -b localhost:19092 -t orders -o beginning -e -q
order-1042 paid 99.00
A production Kafka deployment is a quorum, a controller, partitioned storage across every broker, replication, and a metadata log — plus the disk estate that comes with it. Most teams don't need that. Most teams need a durable, ordered, replayable log they can hand to existing clients.
Every broker owns local JBOD disks, a partition directory tree, and years of partition-reassignment scar tissue. Scaling up means moving terabytes of data between machines.
Replication triples your storage bill to survive a disk failure, and you're paying it for the full retention window — including the 99% of data nobody reads.
ZooKeeper or KRaft, partition leaders, ISR shrink events, under-replicated partitions, consumer group rebalances. Kafka is a product. It's also a platform you now operate.
The agent holds no state worth keeping. The WAL is a write buffer, not a system of record. Sealed segments live in S3, consumer group offsets live in S3, and the metadata checkpoint lives in S3. Kill the agent, start a new one on a different machine, and it picks up exactly where the last one left off. That is what makes it scale like serverless and cost like object storage.
18 Kafka API keys are implemented — Produce, Fetch, ListOffsets, Metadata, the full consumer group lifecycle, topic admin, and SASL PLAIN. MessageSet v0/v1 and RecordBatch v2, with GZIP, Snappy, and LZ4 decoded so offset accounting stays exact.
Your existing clients don't need to know anything changed.
Writes append to a local WAL and acknowledge immediately. A rolling 64 MB segment is sealed and handed to an eight-worker upload pool, with a reconciliation loop as a safety net. Reads check the hot WAL first and transparently fall through to ranged object-store reads using a sparse segment index.
JoinGroup, SyncGroup, Heartbeat, LeaveGroup, and leader-driven assignment with KIP-62 background heartbeats and dynamic rebalancing. Offsets are buffered and flushed to S3, so a restart resumes from the right place instead of reprocessing the world.
Prometheus metrics on :9091 covering request counts and latency
histograms, network throughput, connections, uploader health, and topic/partition
cardinality. Plus SASL/PLAIN auth, time- and size-based retention, and metadata
checkpointing.
Protocol on the front, a storage engine underneath, and a coordinator in the middle. The write path never blocks on the network.
Records land in the partition's local WAL and the producer gets its ack. No network round trip to object storage on the hot path.
The active segment rolls, the seal event is pushed onto a buffered channel, and eight upload workers pick it up in parallel.
Consumers tail the WAL for live data and transparently fall back to range reads against S3 for history. One API, two tiers.
Grafana Mimir 3.0 made ingest storage architecture stable and the recommended way to run: Kafka sits between distributors and ingesters, and the write path ends at Kafka. Grafana Tempo's microservices mode uses a Kafka-compatible queue as the durable write-ahead log behind every write. Both documents point at the same door — the Mimir docs say its use of the Kafka protocol is deliberately limited precisely so you can bring a compatible system instead of Apache Kafka.
That is the workload Kimistore was built for: a write path that must be durably acknowledged, replayed by several independent consumers, and retained for a window measured in hours or days — on storage that costs pennies per gigabyte.
The distributor hashes each series into a topic partition, produces with
acks=all, and does not reply to the client — be that an OTel collector
or a Prometheus remote-write sender — until the broker confirms. Tempo does the
same and documents it as "the Kafka protocol's strongest acknowledgment mode".
The ack is a real durability promise: the batch is fsynced to the local WAL before the offset is returned, with concurrent producers sharing a single flush. Sealed segments are offloaded to object storage in the background, and flushed on shutdown, so an ephemeral local disk loses nothing.
Every Mimir ingester runs its own consumer group, so a zone can be
restarted or rolled back independently. Tempo runs three — block-builder,
live-store and metrics-generator — each tracking its own
offsets at its own pace. One lagging group never blocks another.
Mimir's classic architecture keeps ingesters heavily stateful: they combine in-memory data with local write-ahead logs and sit on both the write and the read path, so heavy queries disrupt live writes. Ingest storage architecture decouples the two, and the write path ends at Kafka — a push succeeds once the broker confirms, without an ingester quorum.
Metadata for itListOffsets requirement, answered from durable stateconsume-from-position-at-startup resolves to earliest/latest/a timestamp through the same APIIn this architecture the broker is a durable hand-off point, not a queryable store — the samples are written again downstream into per-tenant TSDB blocks. A node that can be torn down and recreated is therefore no longer a liability.
In microservices mode Tempo uses a Kafka-compatible queue as the WAL between
distributors and every downstream consumer: block-builders, live-stores and
metrics-generators. Because durability is centralised, Tempo does not have to
replicate data across instances on the write path — it runs with a
replication factor of 1. Monolithic mode
(target: all) pushes in-process and never touches Kafka.
acks=all and wait for backend confirmation before answering the clienttempo_ingest_group_partition_lagTempo's design already runs the write path at replication factor 1. Here that single copy costs object storage rather than three provisioned broker disks.
| What Grafana requires of a Kafka-compatible backend | Who needs it | Kimistore |
|---|---|---|
| Durable acknowledgement before the client is answered | Mimir distributor, Tempo distributor | Yes group-committed fsync for acks>=1 |
| Metadata for topic and leader discovery | Both distributors and ingesters | Yes Metadata v0–v6, configurable advertised listener |
| Several independent consumer groups | Mimir per ingester zone; Tempo block-builder, live-store, metrics-generator |
Yes per-group offsets, persisted to object storage and rehydrated |
| Accurate partition offsets for read consistency and startup positioning | Strong read consistency, consume-from-position-at-startup |
Yes real log start and log end, correct across restarts |
| Replay window measured in hours or days | Block-builder cycle, live-store startup replay | Yes time and size retention on the object-store tier |
| No transactions, no idempotent writes | Both, by design | Neither implemented or required |
| Multi-broker replication across failure domains | Optional for them, never required | Not implemented — by design |
Sources: Mimir ingest storage architecture · Tempo’s Kafka component · Tempo: configure a Kafka-compatible backend
A replicated Kafka ingest path pays for every write three times, plus the broker disks, the controller quorum and the capacity plan. In both Grafana architectures Kafka is a durable hand-off point rather than a queryable store: the data is written again into per-tenant TSDB blocks by the ingesters or block-builders, and read from there. Kimistore keeps exactly one durable copy, in object storage, and exists only to get samples durably acknowledged and replayable. That is the layer where a single-node design is not a compromise but the whole point.
A straight answer, including where Kimistore is the wrong tool.
| Dimension | Apache Kafka | Managed Kafka | Kimistore |
|---|---|---|---|
| Client compatibility | Native | Native | Yes subset of the protocol |
| Coordination | KRaft / ZooKeeper | Vendor-managed | None built-in lite coordinator |
| Deployment unit | Broker cluster | Managed service | One binary |
| Storage system of record | Local JBOD disks | Vendor hardware | S3 / object storage |
| Replication | 3× by default | 3× by default | None — single node |
| Transactions | Yes | Yes | Not supported |
| Best for | Multi-region, high-scale event backbone | Teams that want zero ops at any cost | Single-region streaming, stream-to-lake ingestion, microservices, edge pipelines, dev/staging, cost-sensitive retention |
Kimistore is intentionally designed as a single-node streaming agent backed by S3, eliminating the operational complexity of cluster replication, leader elections, and ISR management. If a workload requires multi-broker active clustering across failure domains with distributed transactions, upstream Kafka is the specialized solution.
Negotiated through ApiVersions one ApiVersions table, generated from the same map the request dispatcher refuses from — so the broker can never advertise a version it then mis-parses.
Requires Go 1.27+, an S3-compatible bucket, and credentials. kcat is the fastest way to see it work.
# From a checkout of github.com/kimistore/agent
$ go build -o agent ./cmd/agent
$ ls -lh agent
-rwxr-xr-x 1 andy staff 27M agent
# Point it at a bucket. Any S3 API store works.
$ export S3_BUCKET=kimistore
$ export AWS_REGION=us-east-1
$ export AWS_ACCESS_KEY_ID=...
$ export AWS_SECRET_ACCESS_KEY=...
$ ./agent
Starting Kimistore Agent...
Metrics listening on :9091
Listening on :19092 # Kafka protocol
# Produce
$ kcat -P -b localhost:19092 -t orders
order-1042 paid 99.00
# Consume from the beginning, then exit
$ kcat -C -b localhost:19092 -t orders -o beginning -e
# Consumer group — open two terminals and watch them split the partitions
$ kcat -b localhost:19092 -G payments orders
# Required
S3_BUCKET=kimistore # default: kimistore
AWS_REGION=us-east-1 # default: us-east-1
# Optional — S3-compatible stores
S3_ENDPOINT=http://localhost:9000 # enables path-style addressing
# Optional — SASL PLAIN. When set, all requests
# require authentication before dispatch.
SASL_USERNAME=admin
SASL_PASSWORD=secret
# Optional — retention. Inert until set.
KIMISTORE_RETENTION_MS=604800000 # 7 days
KIMISTORE_RETENTION_BYTES=-1 # unlimited
KIMISTORE_RETENTION_CHECK_MS=300000 # sweep every 5 min
# Prometheus metrics
curl localhost:9091/metrics
from confluent_kafka import Consumer
c = Consumer({
'bootstrap.servers': 'localhost:19092',
'group.id': 'payments',
'auto.offset.reset': 'earliest',
})
c.subscribe(['orders'])
for msg in c:
print(msg.value())
Properties p = new Properties();
p.put("bootstrap.servers", "localhost:19092");
p.put("group.id", "payments");
p.put("key.deserializer",
StringDeserializer.class);
p.put("value.deserializer",
StringDeserializer.class);
new KafkaConsumer<String, String>(p)
.subscribe(List.of("orders"));
One binary, one bucket. Your integration tests get a real Kafka wire protocol instead of a mock that drifts from reality.
A laptop, a Pi, a spot instance, or a local RustFS. The agent holds nothing durable, so it can be torn down and recreated freely.
Long retention windows on data that is rarely read are exactly the workload object storage wins at. The cold tier costs pennies.
Validate a streaming design against real clients in an afternoon, without a capacity plan, a quorum, or a procurement conversation.
Built from the ground up to deliver Kafka wire-protocol compatibility with object storage economics. Every guarantee is verified by comprehensive durability, crash-recovery, and protocol test suites.
acks>=1), fire-and-forget for acks=0maxWaitMs (capped at 1s) to eliminate client tight polling loopssession.timeout.msKimistore is not a drop-in cluster substitute for multi-datacenter Kafka deployments requiring cross-broker replication, ISR quorums, or distributed transactions. Instead, it is a specialized streaming engine engineered for single-region streaming, stream-to-lake ingestion, microservices, and edge workloads. By combining the Kafka wire protocol with S3 tiered storage in a single binary, Kimistore eliminates cluster operational complexity, ZooKeeper/KRaft maintenance, and expensive broker EBS disks while maintaining seamless compatibility with standard Kafka client libraries.
Sealed segments are already safe in S3. For active writes with acks=1 (or higher), Kimistore uses group-commit fsync to guarantee records are committed to stable disk before acknowledging producers. If the process crashes during an unaligned write, automatic torn-tail truncation on startup detects CRC mismatches and rolls the active segment back to the last intact record, preventing partition corruption. Consumer group offsets are continuously persisted with S3-backed rehydration across restarts and bounded shutdown flush retries. Restarting the agent with the same bucket and storage directory restores all topic metadata, consumer offsets, and high watermarks seamlessly.
A background reaper evicts members that stop heartbeating past their session.timeout.ms, advances the group generation, and re-elects a leader if the leader was the one that died. The remaining consumers rebalance and pick up the orphaned partitions without operator action. Rebalancing waits are bounded too, so a leader that dies mid-rebalance cannot leave followers hanging. After a restart, members restored from a checkpoint are treated as dead, since their connections died with the previous process.
No. Fetch implements long polling. When a consumer is caught up and asks for data, the request parks on an append signal for up to maxWaitMs rather than returning empty immediately, so there is no tight re-poll loop and no wasted round trips. The wait is capped at one second, and clients that set minBytes=0 still get an immediate answer, which is what that setting means.
Yes, and it is worth being precise about why. Honouring acks=1 means one device flush per batch for a single sequential producer, which is a hard floor for any durable broker. Kimistore uses group commit: concurrent producers that arrive while a flush is in flight are all covered by that one flush rather than each paying their own, so parallel producers stay fast. Producers that genuinely do not need durability should use acks=0, which skips the flush entirely. The repository ships benchmarks for both paths so you can measure the trade-off on your own hardware.
For the covered API surface, yes. Kafka clients negotiate capabilities through ApiVersions, and Kimistore advertises exactly what it implements. If your application depends on transactions, idempotence, or ACLs, it will get a clear error rather than silent corruption — but it will get an error.
Yes. Set S3_ENDPOINT (or AWS_ENDPOINT_URL) and the client switches to path-style addressing, which is what most self-hosted S3 API implementations expect. This is what makes local development and air-gapped environments practical.
AGPL-3.0. If you modify it and serve it over a network, you are required to offer your source. That is a deliberate choice for infrastructure software, and the license file in the repository is the authoritative text.
The write path acknowledges after a local WAL append and never blocks on object storage, so producer latency is bounded by local disk rather than S3 round trips. A parallel multi-partition benchmark and a load-test harness ship with the repository — run them against your own hardware and bucket rather than trusting a number on a marketing page.
No cluster to plan, no quorum to babysit. A 27 MB binary and a bucket.