Ordering & Concurrency
Edit this pageSQLStreams claims messages in id order, but concurrent handlers and retries can finish in a different order. Per-key policies determine when a message must wait within one consumer group. They do not order different keys or different groups.
The three policies
| Policy | Same key is active | An earlier same-key message is waiting to retry |
|---|---|---|
parallel | may run concurrently | may run |
exclusive | waits for the key lease | may run before the retry |
ordered | waits | waits until the predecessor is resolved |
Set the policy on the group with ConsumerConfig.ConcurrencyOverride
when every producer must follow it. A per-message policy applies only
to messages that request it: another producer supplying a key without a
policy still requests the default parallel behavior. No warning is
logged for that default.
Ordered Delivery shows the declaration.
MessageConcurrency separately controls an instance’s dispatch capacity;
raising it cannot make one ordered key run concurrently.
Exclusive delivery and compaction
ConcurrencyExclusive uses a message_key_lease row to prevent another
same-key delivery while the lease is held. A busy key produces a
deferred exception row. After a handler error,
the key can be released before that message’s retry is due, allowing a
later message to run first.
For apply-balance on ledger.adjustments, message 101 subtracts 500
from acct-42 and fails; message 102 adds 200. Exclusive delivery can
apply 102 before retrying 101. Choose ordered delivery when that reorder
would change the business result.
With compaction, deferred deliveries also check
which message is the current head. If updates 102 and 103 arrive while
101 runs, 102 can be superseded by 103. That suits a handler applying a
key’s latest state; it does not preserve every delta. ordered refuses
compaction because its guarantee requires preserving that history.
Ordered delivery through a failure
The apply-balance group (id 7) reads ledger.adjustments (stream id
1) with ordered delivery for acct-42:
| Message id | Outcome |
|---|---|
| 101 | handler errors; recorded ready until its retry is due |
| 102 | waits as deferred; handler has not run |
| 103 | waits as deferred; handler has not run |
| 104 | waits as deferred; handler has not run |
The retry of 101 runs first. Success deletes its exception row; permanent
failure or an exhausted retry budget leaves it dead. Either outcome
allows 102 to become eligible, followed by 103 and 104. A requested delay
keeps the predecessor unresolved and the later messages waiting.
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;
What “earlier” means
Ordered delivery checks both recorded exceptions and messages whose ranges have not committed:
exception_queuestores the resolvedmessage_keyandconcurrencyon each row, with an index on(consumer_group_id, message_key, message_id). An earlier unresolved same-key delivery is one indexed lookup: a row with a lowermessage_id, the same key, and statusready,inflight, ordeferred. The policy sits on the row so the exception consumer’s claim can tell an ordered row from an exclusive one without re-resolving options in SQL.- An earlier same-key message the group has claimed but not recorded
yet has no exception row. It sits between the group’s
committedcursor and the message’s own id, so the ordered claim also checksmessage_logfor a same-key id in that window, outside the claimer’s own range. Inside its own range the consumer instance runs same-key ordered messages one after another in id order, in the same goroutine: the first message of a key is dispatched, the rest are chained behind it. When one succeeds or dead-letters the next runs; when one fails, delays, or is deferred, the rest of the chain is recordeddeferredwithout running and the exception consumer takes over in order. An uninterrupted same-key chain can run without an exception poll between its successful handlers.
Both checks live in the key-lease claim. An earlier unresolved delivery
produces a busy verdict and a deferred row. The exception claim also
applies check 1, avoiding repeated claims of a row whose predecessor is
waiting on backoff. Check 2 stays in the key-lease claim until the other
range commits and cursor advancement closes the window.
A deferred ordered message becomes eligible after its predecessor resolves
and waits for an exception poll. A message blocked on another instance’s
range also waits for that range to commit and the cursor advancer to move
committed. Exclusive delivery does not impose these predecessor waits,
though its deferred rows still wait for the key lease and polling.
Lease limits
The policy coordinates claims through leases. An expired lease can be reclaimed even if an old handler ignored cancellation and is still running. Keep handlers idempotent and pass their contexts to blocking calls; Consumer Timeouts explains this at-least-once boundary.
A hot key stays serial regardless of replica count. Different keys can use additional concurrency. For compacted messages, same-key produces also contend on the compaction head until commit.