SQLStreams

the messaging platform that is just Postgres

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

Ordering & Concurrency

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

SQLStreams 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

PolicySame key is activeAn earlier same-key message is waiting to retry
parallelmay run concurrentlymay run
exclusivewaits for the key leasemay run before the retry
orderedwaitswaits 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 idOutcome
101handler errors; recorded ready until its retry is due
102waits as deferred; handler has not run
103waits as deferred; handler has not run
104waits 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:

  1. exception_queue stores the resolved message_key and concurrency on 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 lower message_id, the same key, and status ready, inflight, or deferred. 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.
  2. An earlier same-key message the group has claimed but not recorded yet has no exception row. It sits between the group’s committed cursor and the message’s own id, so the ordered claim also checks message_log for 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 recorded deferred without 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.