Skip to content

Why not Kafka?

PicoMQ serves the Kafka protocol. This page explains why Pico's own data model is not Kafka's. Log workloads fall into two types with different storage requirements.

TypeDescriptionExamples
FunnelingMany producers append to a small number of shared logs. Consumers read each log in fullTelemetry, clickstream ingestion, warehouse loads
RoutingEach entity has its own ordered log. Consumers read individual logs by nameMessaging, feeds, per-entity state, agent sessions

Both need a durable log. Kafka supports funneling only. PicoMQ supports both.

Why Kafka is an excellent funnel

Kafka was built in 2011 to move telemetry from LinkedIn's servers into Hadoop. Early messages carried no keys, and the original implementation discarded the partitioning key after computing a partition. The data model reflects that origin: a small number of wide topics, each split into partitions for parallelism, with consumers reading every partition in order.

serviceserviceservicetopic: user-eventspartition 0partition 1partition 2consumerreads everythingfunneling: many producers, few wide topics, consumers read everything

Storage systems can be measured by read, write, and space amplification, and improving all three requires restricting the access pattern. Kafka restricts in favor of the funnel. Records land in append-only, immutable segment files that are deleted wholesale when they age out of retention, so write amplification is about 1x. The only read API is a sequential scan in write order, so a full scan also costs about 1x read amplification. For funneling this is close to optimal.

Why Kafka is a bad router

Routing inverts the access pattern. Consumers read a specific subset of the data, usually one entity: one user, one session, one workflow run. The underlying storage needs hundreds of thousands or millions of small ordered logs rather than one large one.

Partitions are too heavy to give each entity its own, so entities are interleaved into shared partitions. The partition key controls placement, not access, and there is no read path that returns one key's records. Reconstructing one entity requires scanning the partition and discarding the rest, so read amplification is the ratio of the partition size to the records requested:

             |partition|
  α_read = ─────────────
              |record|
topic: user-eventsp0p1p2readerwants user 1042user 1042's records (accented) are spread across all partitions

This is close to the worst case, and the same layout causes problems beyond reads. Offsets are tracked per partition, not per entity, so one bad record blocks every entity behind it. Changing partition counts moves data and disrupts every consumer. A hot key produces a hot partition that cannot be split in place.

The industry's answers

Most Kafka alternatives compete on the funnel: cheaper brokers, tiered storage, object-native backends. The primitive is unchanged.

Some newer systems address routing by changing the storage layout. Records are kept in an LSM tree keyed by (key, sequence), so one key's log is found by binary search instead of a partition scan, and millions of keyed logs fit on one node. This fixes read amplification, but the log remains an entry inside a larger structure. It cannot be listed, sized, placed, or deleted on its own, and continuous compaction is required to keep each key's records collocated.

Pico: the stream is the primitive

Pico makes the per-entity log a first class object. Every entity gets its own stream, named like a path and backed by its own log. Idle streams have no cost and a deployment holds millions of them, so unrelated entities never share a log and no compaction is needed to keep an entity's records together.

producerwrites one streamprefix: /users//users/1042/users/1043/users/1044 ...router readtail one streamfunnel readlist prefix, fan inone stream per entity, aggregate reads list the prefix and fan in

Producers write to a named stream, so there is no partitioner. Reading one entity means tailing one stream, which costs 1x read amplification. Reading the aggregate means listing a prefix and fanning in across its streams, scoped to one entity, one customer's subtree, or everything.

Partitions (Kafka)Streams (Pico)
Point readsFinding one entity's records requires scanning the whole partitionThe entity is the stream. It is addressed by name and only its records are read
IsolationOffsets are per partition, so one bad record blocks every entity behind itOffsets and failures are scoped to one stream
RescalingChanging partition counts moves data and disrupts all consumers. Hot partitions cannot be split in placeThe stream is the unit of placement. A hot stream moves to another node on its own and offsets do not change
LifecycleRetention applies per topic. One entity's data cannot be trimmed or deleted independentlyRetention, trimming, and deletion are per stream operations

Funneling works the same way on PicoMQ. A topic with N partitions is N streams under one prefix. Producers hash to a stream, consumers fan in across the prefix. Reads are 1x sequential and retention deletes whole segments, as in Kafka. Kafka clients connect through the Kafka protocol.