Pub/Sub
Topic subscriptions and fan-out publish. See the Pub/Sub guide for delivery semantics and lifecycle.
Topic subscriptions and fan-out publish. See the Pub/Sub guide for delivery semantics and lifecycle.
See Pub/Sub for the full guide — delivery semantics, subscription lifecycle, and cross-SDK topics.
@queue.subscriber()@queue.subscriber(
topic: str,
name: str | None = None,
queue: str = "default",
durable: bool = True,
**task_kwargs: Any,
) -> Callable[[Callable], TaskWrapper]Register fn as an independent subscriber of topic. The function becomes a
normal task — task_kwargs are forwarded to
@queue.task() — and the subscription is
written to storage when run_worker() starts, or via
declare_subscriptions() in a producer-only process.
| Parameter | Type | Default | Description |
|---|---|---|---|
topic | str | — | Topic to subscribe to. |
name | str | None | Task name | Stable subscription identity. Re-registering the same (topic, name) updates the routing target instead of duplicating. |
queue | str | "default" | Queue the subscriber's delivery jobs go to. |
durable | bool | True | Persist across restarts. False ties the subscription to one worker process — it only registers inside run_worker() and is reaped once that worker stops heartbeating. |
**task_kwargs | Any | — | Any @queue.task() option (max_retries, timeout, middleware, idempotent, ...). |
queue.declare_subscriptions()queue.declare_subscriptions() -> NoneWrite pending durable subscriptions to storage. Called automatically at
run_worker() startup — call it explicitly in a producer-only process (one
that imports subscriber modules but never runs a worker) so publish() sees
the subscriptions. Ephemeral subscriptions are skipped; they need an owning
worker.
queue.publish()queue.publish(
topic: str,
*args: Any,
idempotency_key: str | None = None,
metadata: dict[str, Any] | None = None,
notes: dict[str, Any] | None = None,
priority: int | None = None,
delay_seconds: float | None = None,
max_retries: int | None = None,
timeout: int | None = None,
expires: float | None = None,
result_ttl: int | None = None,
**kwargs: Any,
) -> list[JobResult]Publish a message to topic. Every active subscription receives an
independent job carrying the same (args, kwargs) payload (at-least-once
per subscriber). Returns one JobResult per
delivery — empty when the topic has no active subscribers, a valid pub/sub
no-op.
| Parameter | Type | Default | Description |
|---|---|---|---|
idempotency_key | str | None | None | Dedupes per subscriber — republishing the same key yields no new deliveries for a subscriber that already received it. Salted internally with the subscription name. |
notes | dict | None | None | Structured annotations, validated against the same 15-field cap as enqueue()'s notes — topic and subscription are then stamped into every delivery's notes on top of that. |
priority / max_retries / timeout / expires / result_ttl | — | None | Override every delivery's setting. Unset fields resolve to the subscriber task's own registered default, then the queue default. |
Unrecognized keyword arguments are forwarded as part of the task payload's
kwargs, same as enqueue().
See Log topics for the cursor, at-least-once, and retention semantics.
queue.subscribe_log()queue.subscribe_log(topic: str, name: str) -> NoneRegister a durable log subscription — a named cursor with no handler.
Writes to storage immediately, so register it before the publishes it
should see. Unlike @queue.subscriber(), there's no separate
declare_subscriptions() step.
@queue.log_consumer()@queue.log_consumer(
topic: str,
name: str | None = None,
*,
poll_interval: float = 1.0,
batch_size: int = 100,
on_error: str = "retry",
)Decorator: register a managed consumer of a log topic. It creates the
durable log subscription (like subscribe_log()) and, when run_worker()
starts, spawns a daemon thread that pulls messages, invokes
handler(*args, **kwargs) per message, and advances the cursor — the
read/ack loop you'd otherwise hand-write. The handler may be sync or async.
name defaults to the handler's name.poll_interval — seconds to wait after an empty poll before re-reading.batch_size — max messages pulled per poll.on_error — "retry" (default) leaves a failed message un-acked so the
batch re-reads; "skip" acks past it and continues.@queue.log_consumer("orders", "audit-log")
def audit(order_id: int) -> None:
audit_sink.write(order_id)queue.declare_topic()queue.declare_topic(name: str, *, retention: float | None = None) -> NoneDeclare a log topic so its publishes are retained even with no subscriber
(removing the late-join boundary). retention (seconds) bounds a sub-less
backlog — each stored message expires that long after publish; None keeps
messages until a subscriber consumes them. Idempotent: re-declaring updates
retention.
queue.list_declared_topics()queue.list_declared_topics() -> list[dict[str, Any]]List declared topics. Each dict has name, mode, retention_ms (None
if unbounded), and created_at (Unix ms).
queue.read_topic()queue.read_topic(topic: str, name: str, limit: int = 100) -> list[TopicMessage]Pull up to limit messages after name's cursor, oldest first and
exclusive of it. Each message's payload is decoded with the queue
serializer. Returns an empty list once the subscription is caught up.
At-least-once — process the batch, then advance the cursor with
ack_topic().
queue.ack_topic()queue.ack_topic(topic: str, name: str, cursor: str) -> boolAdvance a log subscription's cursor to cursor (a message id). A
high-water mark — acking an id acks everything up to and including it.
Monotonic: acking an id older than what's already acked is a no-op and
returns False.
queue.lease_topic()queue.lease_topic(
topic: str, name: str, limit: int = 100, visibility: float = 30.0
) -> list[TopicMessage]Per-message alternative to the cursor read. Leases up to limit
available messages for visibility seconds and tracks each individually:
:meth:ack_message when done, :meth:nack_message to redeliver now, or let
the lease expire to redeliver — so one poison message no longer blocks its
siblings. Oldest first; in-flight (leased, un-expired) messages are skipped.
Don't mix with read_topic/ack_topic on the same subscription.
queue.ack_message() / queue.nack_message()queue.ack_message(topic: str, name: str, message_id: str) -> bool
queue.nack_message(topic: str, name: str, message_id: str) -> boolack_message ends a leased delivery (never redelivered); nack_message
makes it available for redelivery immediately. Both return False when there
was no un-acked delivery for message_id.
queue.topic_log_stats()queue.topic_log_stats() -> list[dict[str, Any]]Lag snapshot for every log subscription. Each dict contains topic,
subscription, cursor (None if nothing has been acked yet), lag
(un-acked message count), and oldest_unacked_age_ms (None when caught
up).
TopicMessagefrom flexiq import TopicMessageOne message pulled from a log topic, returned by read_topic() and lease_topic().
| Field | Type | Description |
|---|---|---|
id | str | Message id — pass to ack_topic() (cursor read) or to ack_message()/nack_message() (per-message lease). |
args | tuple[Any, ...] | Decoded positional args from the publish() call. |
kwargs | dict[str, Any] | Decoded keyword args from the publish() call. |
metadata | dict[str, Any] | None | Caller metadata, if any. |
notes | dict[str, Any] | None | Structured notes, if any. |
created_at | int | Unix-millisecond publish time. |
queue.unsubscribe()queue.unsubscribe(topic: str, name: str) -> boolRemove a subscription. Returns False if none matched.
queue.pause_subscription() / queue.resume_subscription()queue.pause_subscription(topic: str, name: str) -> bool
queue.resume_subscription(topic: str, name: str) -> boolStop or resume deliveries without unregistering. Returns False if the
subscription is unknown.
queue.list_subscriptions()queue.list_subscriptions(topic: str | None = None) -> list[dict[str, Any]]List subscriptions — all of them, or one topic's active ones. Each dict
contains topic, name, task_name, queue, active, and durable.
queue.list_topics()queue.list_topics() -> list[str]Distinct topics that currently have at least one subscription.