Ordered Delivery per Key
Edit this pageSet ConsumerConfig.ConcurrencyOverride to sqlstreams.ConcurrencyOrdered
and give every ordered message a MessageKey. The group then waits for
earlier same-key deliveries to resolve before starting later ones.
Declare the group and produce
The apply-balance group processes deltas on ledger.adjustments:
adjustments := client.Stream[BalanceAdjusted]("ledger.adjustments")
ledger := adjustments.Consumer("apply-balance")
consumer, err := ledger.Register(ctx, &sqlstreams.ConsumerConfig{
ConcurrencyOverride: sqlstreams.ConcurrencyOrdered,
})
if err != nil {
return err
}
Assuming the stream and producer are registered, produce each delta with its account key:
produced, err := producer.Produce(ctx, &BalanceAdjusted{Account: "acct-42", Delta: -500},
&sqlstreams.ProduceOptions{MessageKey: "acct-42"})
if err != nil {
return err
}
Declare the policy on the group so it covers messages from every producer. A message key alone does not request ordering when the group has no override. Ordering & Concurrency compares the three policies and the per-message alternative.
Check a blocked key
Suppose apply-balance has group id 7, ledger.adjustments has stream
id 1, and messages 101–104 use acct-42. Message 101 fails; 102–104
wait without running. Inspect that key:
SELECT
message_id,
message_key,
status,
attempts,
can_run_after
FROM sqlstreams.exception_queue_1
WHERE consumer_group_id = 7 AND message_key = 'acct-42'
ORDER BY message_id;
Expect 101 to be ready while waiting for its retry and the later
messages to be deferred. When 101 succeeds or becomes dead, 102 can
run. A delay keeps 101 unresolved. Handler Outcomes
explains how retry and delay budgets can bound that wait.
A dead predecessor releases the next message; it does not mean the predecessor’s business operation succeeded. Monitor dead rows alongside progress and use the dead-letter triage workflow.
What “earlier” means
The guarantee includes earlier same-key messages in other claimed ranges, even before those messages have exception rows. That can make a key wait for another range’s committed cursor. The ordering mechanism explains both checks with the rows and cursor window they read.
Limits and tuning
- Messages without a key remain parallel. Use the same key for operations that need a shared order; different keys and groups have independent order.
- Ordered messages cannot enable compaction. Compaction may supersede older values; ordered delivery must preserve each eligible message.
- An ordered deferred row can wait until a later exception poll after its predecessor resolves. Waiting on another range also includes cursor advancement time.
- More instances do not speed up one serial key. Raise concurrency for independent keys, and use Consumer Tuning to size queues and leases for slow handlers.
Pass the handler context to blocking calls. Lease expiry and a handler that ignores cancellation can still overlap with a reclaimed attempt; Consumer Timeouts covers that boundary.