Raw batch mode: shipping protobuf without a schema registry
For the first four months, Runnev parsed every event you published. We validated JSON, counted events, and stored structured objects. Then we shipped raw batch mode and stopped looking inside payloads entirely. This is why, and what it bought.
The problem with parsing everything
Parsing has three costs that all grow with your traffic. It burns CPU on the ingest path, which is the one path you least want to slow down. It forces a schema opinion onto users who already have one. And it creates a class of rejection, malformed JSON, that has nothing to do with whether the bytes are what the producer meant to send.
The users who felt this first were the ones already serializing with protobuf. They had a schema, a code generator, and wire-efficient bytes, and we were asking them to wrap those bytes in JSON so we could parse the wrapper and ignore the contents. That is pure overhead: base64 inflation, a parse we did not need, and a second schema, ours, layered over theirs.
Raw mode: bytes in, bytes out
A stream created with "mode":"raw" accepts an
application/octet-stream body on publish and stores it verbatim. We do not
parse it, validate it, or transform it. A batch is whatever bytes you sent, and on replay
you get exactly those bytes back with the content type you set.
curl https://runnev.dev/v1/streams/$STREAM/9001 \
-H "Authorization: Bearer $RUNNEV_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @order.pbThere is no schema registry to run and no schema to register. Your producer and your consumer agree on a format the way they always did, out of band, in their own code. We are the pipe, not a party to the contract.
What the ingest path looks like now
For a raw batch, the ingest handler does four things: check auth, check the size limit,
check the sequence, and append the bytes to the ring buffer. There is no
json.Unmarshal, no per-event iteration, no counting. The batch counts as one
event for accounting and the bytes count for throughput and egress.
The throughput difference showed up immediately for large batches. On a 512 KiB batch of a few thousand small records, dropping the parse and the base64 round trip roughly halved the CPU time we spent per batch on ingest, and it removed a whole tail of latency spikes that had correlated with garbage-collection pressure from allocating parsed structures we threw away a millisecond later. We are not going to pretend a single number generalizes, since it depends entirely on payload shape, but the direction was not subtle: the cheapest parse is the one you do not do.
The cost of not looking
Raw mode moves responsibility to you, and we want to be honest about that. We cannot tell
you a raw batch is malformed, because we do not know what formed means for your data. We
cannot count events inside it, so event-based limits treat the batch as one event and
bill on bytes instead. And over the live SSE path, raw bytes are base64-encoded in the
data field, because SSE is a text protocol; the replay endpoint returns the
raw bytes directly, so a consumer that cares about byte efficiency reads from replay.
For structured events where you want the platform to understand your data, json mode is still the default and still the right choice. Raw mode is for when you already own the format and want us out of the way. Shipping both, and letting the stream declare which it is at creation, turned out to be simpler than trying to be clever about auto-detecting, which we tried first and correctly abandoned.