Streams
A stream is an ordered, durable log of batches, identified by a base62 id and bounded by a retention window. This page is the concept in full: identity, the cursor, retention, modes, and the guarantees you can and cannot lean on.
Identity
Every stream is named by a 22-character base62 id, for example
cBczepiZSW8GJaCae0xIj7. There is no prefix and no other id
format. You may let the server generate the id at creation, or you may generate a
base62 id client-side and create the stream with it; supplying your own id makes stream
creation itself idempotent.
The human-readable name is metadata for your own convenience. It is not
unique across a project by default and is never used to address a stream.
The cursor
A stream's cursor is the sequence number of the newest batch it has
accepted. It starts at 0 on an empty stream and only ever moves forward.
Subscribers each track their own position, also called a cursor, which is simply the
sequence of the last batch they processed. "Resume from cursor N" means "give me
everything after sequence N".
{"cursor":41823,"updated_at":"2026-08-04T11:02:13.418Z"}
Retention
A stream keeps batches until they fall outside either of two bounds, whichever is hit first:
retention_seconds- age. A batch older than this is eligible for eviction. Range 60 to 2,592,000 seconds (one minute to thirty days); values outside the range are rejected withretention_out_of_range.max_bytes- total stored size. When the sum of batch sizes would exceed this, the oldest batches are evicted until it fits. Default 256 MiB.
Eviction is from the tail (oldest first) and is the only way batches leave a stream
other than deleting the stream. Eviction advances the effective floor but never the
cursor: sequence numbers of surviving batches do not change. A subscriber
that resumes from a cursor older than the floor receives the oldest surviving batch
next, and the gap shows up as a jump in sequence numbers, which is your signal that it
fell behind the window.
Pick retention from your worst realistic subscriber outage, not your average. If a consumer can be down for two hours during a deploy, an hour of retention will lose data; give it a comfortable margin. There is a worked calculation on the blog under Sizing ring buffers.
json and raw modes
A stream is created in one of two modes, fixed for its lifetime:
| Mode | Publish body | Delivered as | Use for |
|---|---|---|---|
json (default) | {"events":[...]} | JSON batch with an events array | Structured domain events |
raw | application/octet-stream bytes | Base64 in the SSE data field, or verbatim bytes on replay | protobuf, msgpack, Avro, your own framing |
In raw mode Runnev never parses your payload. See
Publishing for both paths.
Lifecycle: create, inspect, delete
Create
curl https://runnev.dev/v1/streams \
-H "Authorization: Bearer $RUNNEV_API_KEY" \
-d '{"name":"orders-eu","retention_seconds":86400,"mode":"json"}'
Only name is required. A duplicate name is allowed; if you
want name uniqueness, enforce it on your side or generate the id yourself. Reusing an
id you already created returns 409 with stream_name_taken.
Inspect
A plain GET on a stream (no Accept: text/event-stream)
returns its metadata:
{
"id": "cBczepiZSW8GJaCae0xIj7",
"name": "orders-eu",
"created_at": "2026-06-11T09:22:31Z",
"retention_seconds": 86400,
"max_bytes": 268435456,
"cursor": 41823,
"bytes_stored": 118293011,
"mode": "json"
}
List all streams in the project with GET /v1/streams, which paginates
with ?limit= and ?cursor= (a pagination token, unrelated to a
stream's sequence cursor).
Delete
curl -X DELETE https://runnev.dev/v1/streams/$STREAM \
-H "Authorization: Bearer $RUNNEV_API_KEY"
# 204 No Content
Deletion is immediate and permanent. Open subscribers receive a terminal
event: end and the connection closes.
Guarantees
What you can rely on:
- Order. Within a stream, batches are delivered in sequence order.
- Durability within retention. An accepted batch is readable by any subscriber until it is evicted by the retention rules.
- At-least-once delivery. A subscriber that resumes correctly from
its cursor sees every surviving batch at least once. A reconnect around a network
blip may redeliver the last batch; dedup on
seq. - Idempotent writes. Publishing the same
(stream, seq)twice stores it once.
What is explicitly not guaranteed:
- No exactly-once. See at-least-once above.
- No per-consumer acknowledgement or redelivery. There is no server-side notion of a consumer having processed a batch.
- No cross-stream ordering. Two streams have no defined relative order. If you need a global order, use one stream.
- No dead-letter handling. A batch you cannot process is your concern; Runnev does not quarantine it.