SQLStreams

the messaging platform that is just Postgres

You last visited on 9999-99-99 Show what's new since then

Consumer

Edit this page
Posted: 2026-09-12 · Report this thread
brandon Site Admin brandon profile Posts: 677

stream.Consumer(name) names a consumer group on its stream: no I/O, no failure. Register writes the group’s declaration and returns the ConsumerInstance[T] whose Consume runs a session. What the group means is declared at Register and stored on the group’s rows; how one session runs is passed to Consume (consumer group config explains the split).

payments, err := client.Stream[PaymentRequestedV1]("payments.requested").Consumer("charge-cards").Register(ctx,
	&sqlstreams.ConsumerConfig{
		Message: &sqlstreams.MessageOptions{
			Timeout: 10 * time.Second,
			Retry:   &sqlstreams.RetryPolicy{MaxRetries: 3, BaseDelay: 2 * time.Second},
		},
	})
if err != nil {
	return err
}

return payments.Consume(ctx, func(ctx context.Context, payment *PaymentRequestedV1) error {
	fmt.Printf("charging %s\n", payment.OrderId)
	return nil
}, &sqlstreams.ConsumeOptions{BatchLimit: 20})

Verbs

On the handle:

verbreturnsnotes
Register(ctx, cfg)*ConsumerInstance[T]resolves the stream, writes the declaration, creates the cursor row on first registration; nil cfg is the defaults; newest declaration wins, and a differing one logs SQL0059
Get(ctx)*Consumerthe comma-ok read: (nil, nil) when the stream or the group is not registered
Workers(ctx)[]*Workerthe group’s own worker rows, each with its stored config document in Worker.Metadata
Destroy(ctx, options)errordeletes the group’s cursor, bindings, leases, delivery rows, workers, and schedules; the stream and its messages stay; ErrDestroyDisabled unless ClientConfig.AllowDestroy; ErrConsumerGroupLive while an instance is live and ErrConsumerGroupDeliveriesPending while delivery rows remain, both skipped by DestroyOptions.Force
Binding()*BindingHandleno I/O; its Get(ctx) reads the group’s effective binding set, (nil, nil) when the group never declared one and reads the whole stream
Metrics()*ConsumerMetricsHandleno I/O; Metrics
Alerts()*ConsumerAlertsHandleno I/O; Alerts

On the instance:

verbreturnsnotes
Consume(ctx, consumerFunc, options)errorblocks for the session; cancel ctx to start graceful shutdown and get nil back; nil options is the defaults; a second call on the same instance returns ErrAlreadyConsuming

consumerFunc is func(ctx context.Context, message *T) error, and its return value is the delivery’s outcome: nil succeeds, any error retries, sqlstreams.Terminal(err) dead-letters now, sqlstreams.Delay(d) runs later without counting a failure (handler outcomes). sqlstreams.MetaFromContext(ctx) inside the handler returns the delivery’s MessageMeta: message id, routing and message keys, attempt count, and the resolved options it runs under.

Consume runs the system manager beside the session unless ClientConfig.DisableManager is set, so one live consumer keeps the whole deployment’s upkeep running (Manager). A context that can never be cancelled returns ErrLifecycleContextNotCancellable unless DisableGracefulShutdown is set, and that case runs no manager.

CLI

commandclient operation
consumer list orders.createdStream(name).Consumers(ctx)
consumer get orders.created billingConsumer(name).Get(ctx)
consumer worker list orders.created billing [key]Consumer(name).Workers(ctx), displaying the stored config keys per worker

List accepts --quiet for names only. Get accepts --quiet for an existence check: no output, exit 0 when registered, exit 1 when absent. Both accept --output json; list returns the consumer array, and get returns the consumer row or null with exit 1 when absent. --quiet and --output json cannot be combined.

Worker list accepts an optional stored config key, such as exception_initial_backoff or message.timeout. Its JSON document contains stream, consumer, and keys, each with its worker name and stored value. Session settings such as ConsumeOptions.ClaimPollRate are not stored worker config. Workers are declared at Register; running instances refresh their stored settings at ConfigRefreshInterval.

Config

ConsumerConfig

Declared at Register, stored on the group’s worker_config rows, the same for every instance.

fielddefaultwhat it decides
Messagetimeout 30s, retry MaxRetries: 3 on the default curvethe MessageOptions filling whatever the produced message left unset
MessageMinnilper-option floors on what a message may request
MessageMaxMessage’s valuesper-option ceilings; a message cannot request above the group’s defaults unless raised here
ConcurrencyOverride"" (honor the message’s own)run every message under this policy: ConcurrencyParallel, ConcurrencyExclusive, ConcurrencyOrdered
Startsqlstreams.Beginning()where a new group’s cursor is placed; read once, at creation (where a new group starts)
Bindingsnil (the whole stream)the group’s whole pattern set (routing)
ExceptionInitialBackoff5sthe first can_run_after delay on a fresh exception row
MaxRangeReclaims3reclaims after which a range is quarantined

ConsumeOptions

Passed to Consume, one session’s own.

fielddefaultwhat it decides
BatchLimit4messages claimed per poll
QueueSizeBatchLimitclaimed messages buffered ahead of processing; at least BatchLimit
MessageConcurrency1messages this instance processes at once
ClaimPollRate500mshow often an idle instance polls
QueueMargin15slease padding for time a claim sits queued
RecordMargin2slease padding for recording the outcome
TimeoutGrace100msslack for a handler that respected ctx.Done() to unwind before the hard cutoff abandons it
SlowDispatchThreshold0 (off)a dispatch running longer logs SQL0039
InstanceTTL30show long this instance’s worker_instance rows stay live without a heartbeat
BindingRetryInterval10show often a waiting binding declaration is retried
ConfigRefreshInterval30show often the stored group config is re-read: the staleness window for a redeclaration
ShutdownTimeoutMessageMax.Timeout + TimeoutGrace + RecordMarginhow long the drain waits for in-flight handlers before the rest is released
DisableGracefulShutdownfalseaccept a context that can never be cancelled, leaving process exit as the only stop

Queue and lease budgets

See consumer tuning for workload-specific starting settings and the measurements to compare before raising them.

The default consumer claims four messages at a time and buffers up to four ordinary messages ahead of processing. Handlers remain serial unless MessageConcurrency is raised. Idle claim and exception polling share ClaimPollRate; a shorter interval also increases database query traffic.

The default range lease is 30s + 100ms + 15s + 2s = 47.1s. A crash can leave claimed messages waiting that long before a later poll reclaims them. ShutdownTimeout remains 32.1s: it covers in-flight processing, while QueueMargin covers waiting before processing.

For example, charge-cards processing ids 1–8 with a three-second handler can leave the next four messages waiting roughly twelve seconds. The fifteen-second margin covers that wait with some room for database work. Longer handlers need a smaller batch and queue or a larger QueueMargin. QueueSize must stay at least BatchLimit. An explicit queue below 4 requires an explicit compatible batch limit; set both to 1 for shallow prefetch. Choose the handler timeout from legitimate runtime; raising it and MessageMax.Timeout together does not increase queue allowance.

Ordered messages on one key remain serial and can wait for the committed cursor between ranges. ClaimPollRate is not a delivery-latency guarantee.

The Info-level starting log includes resolved session settings and the registration-time timeout budgets. Stored group settings are read when consumer workers start and on config refresh. SQL0105 warns when a queued message has insufficient lease time to start. Existing warning suppression collapses repeated warnings within the instance’s one-minute window.

Gotchas

  • MessageConcurrency is how many messages this instance runs at once; ConcurrencyOverride is what a message key means. They sound alike and sit on opposite sides of the split.
  • A group’s config wants exactly one declaring service. Two services declaring charge-cards differently overwrite each other on every restart, and the same SQL0059 line on every restart is the tell.
  • Handlers should be idempotent: redelivery after a crash or timeout is normal at-least-once behavior (side effects and retries).
  • Worker.Metadata is the stored document, sparse: an absent timeout means it was omitted, not that deliveries have no timeout. Defaults are resolved when an instance reads it, and worker-specific fields are outside the stable contract.