Skip to main content

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 appliesWhat dedup catches
id (envelope)Service Bus onlyA redelivery of the same emission. Identical bytes.
Screening id (data.id on Service Bus, id on REST)Service Bus and the Sessions APIA 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 CompleteMessage only after you've durably persisted the work.
  • Idempotency-key on data.id; ignore duplicates.
  • Sort by data.testTime if order matters.
  • Set a peek-lock duration longer than your worst-case processing time (or call RenewMessageLock periodically).
  • Watch the dead-letter sub-queue and alert on growth.