SQLStreams

the messaging platform that is just Postgres

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

Ordered Delivery per Key

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

Set 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.