Skip to content

Streams

Streams are the durable cousin of pub/sub. Every event is a row in _honker_stream, consumers track offsets in _honker_stream_consumers, and replay-on-reconnect is built in. Use streams when you want at-least-once delivery or catch-up after a disconnect. Use pub/sub when you only care about live signals and don’t need history.

The on-disk format is identical across bindings: a Go publisher and a Python consumer can run against the same .db file.

import honker
db = honker.open("app.db")
s = db.stream("orders")
s.publish({"id": 42, "amount": 9900})
# Atomic with a business write
with db.transaction() as tx:
tx.execute("INSERT INTO orders VALUES (?, ?)", [42, 9900])
s.publish({"id": 42, "amount": 9900}, tx=tx)

A named consumer resumes where it left off. The iterator amortizes offset saves across events so the single-writer slot doesn’t thrash.

async for event in s.subscribe(consumer="email-worker"):
await send_order_email(event.payload)
# Offset auto-saves on a cadence (default: every 1000 events or 1 second).

For exactly-once-within-a-business-transaction semantics, save the offset in the same transaction as the downstream write. If the transaction rolls back, the offset doesn’t advance and the event replays on the next read.

# Python
async for event in s.subscribe(
consumer="invariant-writer",
save_every_n=0, # disable auto-save
save_every_s=0,
):
with db.transaction() as tx:
apply_to_read_model(event.payload, tx=tx)
# Stream.save_offset has no tx parameter; use the extension
# function directly so the offset lands in the same transaction.
tx.query(
"SELECT honker_stream_save_offset(?, ?, ?)",
["invariant-writer", s.name, event.offset],
)

(Calling s.save_offset("invariant-writer", event.offset) without tx= also works, but it commits outside your transaction and the atomicity is lost.)

Other bindings pass the tx handle into their own save-offset call, or run honker_stream_save_offset inside the open transaction the same way — the extension-level SQL runs inside whatever transaction is open.

These options are Python-only — Stream.subscribe(consumer=..., from_offset=...). In Node and Bun the signature is subscribe(consumer, opts); calling it with no consumer crashes, and there is no from_offset argument.

  • subscribe(consumer=name) starts from the saved offset for this consumer (0 if never saved).
  • subscribe(from_offset=N) starts after offset N, regardless of any saved offset.
  • subscribe() with no arguments replays from offset 0 — a full replay of the topic. There is no live-tail-from-now mode in Python.
  • _honker_stream is the append-only event log. A (topic, offset) index keeps reads O(log n) even with millions of historical events.
  • _honker_stream_consumers stores (name, topic) -> offset. One row per consumer per topic.