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.

Realtime notifications

Realtime notification systems need to push events from multiple sources (payment service, order service, auth service) to multiple consumers (mobile push workers, email queues, analytics pipelines). tinybroker acts as a lightweight event bus between these services, decoupling producers from consumers without requiring a dedicated platform.

Simple notification pipeline

Payment service ──► tinybroker ──► Notification worker (push/email)
Order service   ──►                Analytics pipeline
Auth service    ──►                Audit logger

Each source service publishes to domain-scoped topics. Each consumer subscribes to the topics it cares about.

Topic design

Use a hierarchical naming scheme that allows both broad and narrow subscriptions:

payment.charge.succeeded
payment.charge.failed
payment.refund.created
order.placed
order.shipped
order.cancelled
auth.login.success
auth.login.failed
auth.password.reset

A notification worker subscribes to payment.* and order.*. An audit logger subscribes to auth.* and payment.*. A fraud detector subscribes to auth.login.failed and payment.charge.failed.

Push notification worker

import grpc
from tinybroker.v1 import service_pb2_grpc, client_pb2, shared_pb2

channel = grpc.insecure_channel("tinybroker:50051")
stub = service_pb2_grpc.BrokerStub(channel)

def subscribe_commands():
    yield client_pb2.SubscribeCommand(
        open=client_pb2.OpenSubscription(
            topic_patterns=["payment.*", "order.placed", "order.shipped"],
            mode=shared_pb2.DELIVERY_MODE_BALANCED,
            consumer_group="push-workers",
        )
    )

for event in stub.Subscribe(subscribe_commands()):
    if event.HasField("message"):
        handle_notification(event.message.topic, event.message.payload)
        stub.Ack(AckRequest(
            subscription_id=subscription_id,
            message_id=event.message.message_id,
        ))

Using DELIVERY_MODE_BALANCED and a shared consumer_group ensures that each notification is processed exactly once across all worker replicas — no duplicate pushes.

Multiple independent consumers

Different consumers use different subscription_id values and DELIVERY_MODE_MULTI so each gets every event independently:

Consumersubscription_idModePatterns
Push worker poolpush-workersBALANCEDpayment.*, order.*
Analyticsanalytics-sinkMULTI*.*.*
Audit logaudit-loggerMULTIauth.*, payment.*
Fraud detectorfraud-detectionMULTIauth.login.failed, payment.charge.failed

Delayed and scheduled notifications

tinybroker does not support delayed delivery. For scheduled notifications (send in 30 minutes), publish immediately to a topic and let the consumer store the event with its target delivery time, then use a separate cron/scheduler. tinybroker handles the fan-out; your consumer handles the timing.

Durability considerations

tinybroker is in-memory. Events published while a consumer is disconnected are lost. For guaranteed delivery:

  • Keep the consumer subscription always connected (reconnect on drop).
  • If the broker restarts, events published during the restart window are lost. Use a database outbox table as the authoritative source and tinybroker as a real-time accelerator: write to DB first, publish to broker second.