Single-node · In-memory · gRPC

Heads up: I ended up switching to NATS + PostgreSQL. If you need consumer groups, bare NATS won't help — you need JetStream, which requires persistence. tinybroker fills the gap when you want in-memory consumer groups with no persistence overhead. The code works and is free to use; just know why you're reaching for it.

Broadcast (fan-out)

Each subscriber opens a subscription with its own unique subscription_id (or leaves it blank to generate one). The broker delivers every matching message to all active subscribers independently.

This is the default mode and requires no configuration beyond choosing your topic patterns.

When to use

  • Push a config reload signal to every replica simultaneously.
  • Fan-out a WebSocket event to all backend nodes that maintain live connections.
  • Propagate invalidation events to distributed in-process caches.
  • Broadcast a task to every worker that should act on it (all-or-nothing fan-out).

Example

Three replicas all subscribe to deploy.*. One publisher calls Publish(topic: "deploy.rollout"). All three receive the message at the same time.

Publisher          tinybroker          Replica A
    │                  │                   │
    │── Publish ───────►                   │
    │    "deploy.rollout"                  │
    │                  │── Message ────────►
    │                  │    "deploy.rollout"
    │                  │
    │                  │               Replica B
    │                  │── Message ────────►
    │                  │    "deploy.rollout"
    │                  │
    │                  │               Replica C
    │                  │── Message ────────►
    │                       "deploy.rollout"

Protocol

Open a subscription — no consumer_group needed:

OpenSubscription {
  topic_patterns: ["deploy.*"]
  mode: DELIVERY_MODE_MULTI
}

Each replica uses its own unique subscription_id. The broker maintains a separate delivery channel per subscription, so a slow replica does not block others.

Backpressure

Each subscriber has its own bounded channel (consumer_capacity slots in config). If a subscriber falls too far behind, the broker evicts it and the subscription stream closes with None. Reconnect and re-subscribe to resume.