Delivery semantics
This page tells you what guarantees the queue makes (and which it doesn't) so you can build a consumer that's correct under all of them.
At-least-once delivery
Service Bus delivers each event to your consumer at least once. In practice that means you may receive the same CloudEvent more than once if:
- Your consumer received the message but crashed before calling
CompleteMessage(the broker re-locks and redelivers). - Your consumer abandoned the message explicitly (returned it for redelivery).
- The peek-lock expired before you completed it.
Each redelivery is a new Service Bus message but carries the same
CloudEvent JSON in its body — so the envelope id, the data.id, and
every other field are identical to the original.
Dedup contract
On Service Bus, dedup on the envelope id at the receive boundary,
and on the screening id (which is data.id inside the
CloudEvent) at the application layer. The Sessions API has no envelope,
so only the application-layer key applies there.
| Why two keys? | Where it applies | What dedup catches |
|---|---|---|
id (envelope) | Service Bus only | A redelivery of the same emission. Identical bytes. |
Screening id (data.id on Service Bus, id on REST) | Service Bus and the Sessions API | A redelivery or receiving the same session again from the Sessions API. |
If you maintain a "have I processed this session before?" check keyed on
the screening id (data.id on Service Bus, id on REST), you get
correct behavior whether the duplicate came from Service Bus retry or
from a Sessions API backfill. Key on the screening id in a durable
store of your choice and treat duplicates as not-failures.
Ordering
Per-queue FIFO is not guaranteed in the v1 contract.
In practice, the broker delivers in roughly publish order, but redelivered messages can land out of order with respect to messages published after them. Do not rely on receive order to derive any business meaning.
If you need a deterministic order for processing — for example, to apply
events in the order tests were performed — sort received events by
data.testTime (then data.id as tiebreaker) and reconcile in your own
storage.
This is the same ordering the Sessions API applies internally; both
transports give you data.testTime-ordered events when you sort.
Retention
We don't set a TTL on the queues we provision for you, so messages persist until your consumer receives and completes them — the broker won't expire them. The practical bound is the queue's storage quota.
For cold starts, replays, or any case where you need history independent of your queue position, use the Sessions API.
Failure handling
Service Bus tracks a delivery count per message. If your consumer abandons the same message many times in a row, Service Bus will eventually dead-letter it (move it to the queue's dead-letter sub-queue). The dead-letter sub-queue is not drained automatically by us — we deliberately leave failed messages there so you can inspect them.
In ordinary operation we'd expect the dead-letter sub-queue to be empty or near-empty.
Retries on the producer side
If our publication step fails (for example, the broker is briefly
unavailable while we're trying to emit), Listo's backend retries with
exponential backoff. Successful retries can produce duplicate emissions
with different envelope ids. Even in that case, data.id is the
same and the application-layer dedup pattern recovers.
Practical checklist for a correct consumer
- Use peek-lock receive mode; call
CompleteMessageonly after you've durably persisted the work. - Idempotency-key on
data.id; ignore duplicates. - Sort by
data.testTimeif order matters. - Set a peek-lock duration longer than your worst-case processing time
(or call
RenewMessageLockperiodically). - Watch the dead-letter sub-queue and alert on growth.