Pub/Sub
Honker’s pub/sub is a direct analog of Postgres’s pg_notify / LISTEN: fire-and-forget signals between processes that share a SQLite file. The Python binding measured 2.9 ms p50 cross-process wake latency on an Apple M1 Pro over 500 samples. No replay, no delivery guarantees. For that, use Streams.
The wire format is the same across bindings: a Python publisher and a Go listener talk to the same channel through the same .db file.
Fire a signal
Section titled “Fire a signal”import honker
db = honker.open("app.db")
with db.transaction() as tx: tx.notify("orders", {"id": 42, "event": "placed"})The notification commits with the surrounding transaction. If the tx rolls back, no one receives it.
const { open } = require('@russellthehippo/honker-node');const db = open('app.db');
// Atomic with a business writeconst tx = db.transaction();tx.execute("INSERT INTO orders (id) VALUES (?)", [42]);tx.notify('orders', { id: 42, event: 'placed' });tx.commit();
// Or fire-and-forget outside a txdb.notify('orders', { id: 42, event: 'placed' });use honker::Database;use serde_json::json;
let db = Database::open("app.db")?;db.notify("orders", &json!({"id": 42, "event": "placed"}))?;import honker "github.com/russellromney/honker-go"
db, _ := honker.Open("app.db", "./libhonker_ext.dylib")defer db.Close()
db.Notify("orders", map[string]any{"id": 42, "event": "placed"})require "honker"
db = Honker::Database.new("app.db", extension_path: "./libhonker_ext.dylib")db.notify("orders", {id: 42, event: "placed"})import { open } from "@russellthehippo/honker-bun";
const db = open("app.db", "./libhonker_ext.dylib");db.notify("orders", { id: 42, event: "placed" });{:ok, db} = Honker.open("app.db", extension_path: "./libhonker_ext.dylib")Honker.notify(db, "orders", %{id: 42, event: "placed"})using Honker;
using var db = Database.Open("app.db");db.Notify("orders", new { id = 42, @event = "placed" });
// Or call tx.Notify(...) inside a Honker transaction.import dev.honker.*;
try (Database db = Honker.open("app.db")) { db.notify("orders", "{\"id\":42,\"event\":\"placed\"}");}import dev.honker.kotlin.honker
honker("app.db").use { db -> db.notify("orders", """{"id":42,"event":"placed"}""")}#include "honker.hpp"
int main() { honker::Database db{"app.db", "./libhonker_ext.dylib"};
honker::Transaction tx{db.raw()}; sqlite3_exec(tx.raw(), "INSERT INTO orders (id) VALUES (42)", nullptr, nullptr, nullptr); sqlite3_exec(tx.raw(), "SELECT notify('orders', '{\"id\":42,\"event\":\"placed\"}')", nullptr, nullptr, nullptr); tx.commit();
db.notify("orders", R"({"id":42,"event":"placed"})");}.load ./libhonker_extSELECT honker_bootstrap();
-- Matches pg_notify's signature for drop-in familiarity.SELECT notify('orders', '{"id":42,"event":"placed"}');Listen
Section titled “Listen”The listener starts from MAX(id) at attach time. Historical notifications are not replayed. Honker runs a shared commit-poll thread at 1 ms cadence (one PRAGMA data_version read per database, counter increments on every commit from any connection in any journal mode), so every listener in the process wakes on any commit and then filters by channel at the SELECT level.
async for notif in db.listen("orders"): print(notif.channel, notif.payload)for await (const notif of db.listen('orders')) { console.log(notif.channel, notif.payload);}// Blocking iterator; wakes on any commit to the db.for notif in db.listen("orders")? { let notif = notif?; println!("{} {}", notif.channel, notif.payload);}ctx, cancel := context.WithCancel(context.Background())defer cancel()
for notif := range db.Listen(ctx, "orders") { fmt.Println(notif.Channel, string(notif.Payload))}db.listen("orders") do |notification| puts "#{notification.channel} #{notification.payload}"endfor await (const notif of db.listen("orders")) { console.log(notif.channel, notif.payload);}{:ok, subscription} = Honker.listen(db, "orders")ref = subscription.ref
receive_loop = fn loop -> receive do {:honker_notification, ^ref, notification} -> IO.inspect({notification.channel, notification.payload}) loop.(loop) endend
receive_loop.(receive_loop)await foreach (var notification in db.Listen("orders")){ Console.WriteLine($"{notification.Channel} {notification.PayloadRaw}");}import java.time.Duration;
try (Listener listener = db.listen("orders")) { while (true) { listener.next(Duration.ofSeconds(15)).ifPresent(notification -> System.out.println(notification.channel() + " " + notification.payloadJson()) ); }}import dev.honker.kotlin.asFlow
db.listen("orders").asFlow().collect { notification -> println("${notification.channel()} ${notification.payloadJson()}")}#include "honker.hpp"
#include <chrono>#include <iostream>
int main() { honker::Database db{"app.db", "./libhonker_ext.dylib"}; auto sub = db.listen("orders");
for (;;) { if (auto notif = sub.recv(std::chrono::milliseconds(5000))) { std::cout << notif->channel() << " " << notif->payload() << "\n"; } }}Cross-process
Section titled “Cross-process”The commit watcher fires on commits from any process. A FastAPI app, a CLI worker, and a cron job can all notify() the same channel, and a separate listener receives every signal within a few milliseconds. No coordination, no broker, no extra network hop.
Pruning
Section titled “Pruning”Notifications accumulate in _honker_notifications until you prune them. There’s no auto-prune because “how much history do you want” is a product decision.
| Binding | Typed age pruning | Typed max-count pruning |
|---|---|---|
| Python, Node, Rust, .NET, Java, Kotlin | yes | yes |
| Ruby, Elixir | yes | no |
| Go, Bun, C++ | no | no |
Every binding can use the raw-SQL fallback below.
# Pythondb.prune_notifications(older_than_s=3600) # delete rows older than 1 hourdb.prune_notifications(max_keep=10_000) # keep only the most recent 10k// Nodedb.pruneNotifications(3600); // delete rows older than 1 hourdb.pruneNotifications(null, 10000); // keep only the most recent 10kdb.PruneNotifications(olderThanSeconds: 3600);db.PruneNotifications(maxKeep: 10_000);// Java (Kotlin uses the same JVM API or notificationPruneOptions { ... })db.pruneNotifications(NotificationPruneOptions.builder() .olderThan(Duration.ofHours(1)) .maxKeep(10_000) .build());There are no honker_prune_* SQL functions — pruning is a binding-level helper. From raw SQL, delete against the table directly:
DELETE FROM _honker_notifications WHERE created_at < unixepoch() - 3600;