Skip to content
ContextSystems
Field notes

MQTT 5 Sessions Under Failure: What the Broker Keeps

A failure-oriented field note on MQTT 5 session state, QoS delivery, and reason codes, run against eclipse-mosquitto 2.0.22 with the raw transcript retained.

Mellisa Waltzer7 min read

Grounding class: LAB-RUN. The behavior below was run on Docker 29.6.1 with eclipse-mosquitto:2.0.22, pinned to image digest sha256:212f89e1eaeb2c322d6441b64396e3346026674db8fa9c27beac293405c32b3c. The raw command transcript is in evidence/mqtt5-session-failure-semantics-verification.txt.

MQTT delivery guarantees depend on more than the QoS number attached to a publication. Session state, client identity, expiry, and application processing determine what survives a disconnect. If you test only a connected publisher and subscriber, you have not tested the failure path.

The test boundary

The lab used one Mosquitto 2.0.22 broker in Docker. The broker stayed in its default local-only mode. Every client command ran inside the same container against 127.0.0.1. No service was exposed on a host interface. The container was removed at the end.

The run exercised three behaviors:

  • A persistent MQTT 5 client subscribed at QoS 1, disconnected, and later received a publication sent while it was offline.
  • A second offline publication carried a one-second Message Expiry Interval and was not delivered after a two-second wait.
  • The first client then connected with Clean Start and a zero Session Expiry Interval, replacing its stored session.

These observations establish what happened in this pinned run. They are not performance results and do not establish behavior for another broker or configuration.

Session state is the delivery boundary

The MQTT Version 5.0 OASIS Standard separates the network connection from the session. Section 4.1 defines session state, including subscriptions and unacknowledged QoS 1 and QoS 2 messages. Sections 3.1.2.4 and 3.1.2.11 define Clean Start and Session Expiry Interval.

That separation is visible in the transcript. The lab-durable client connected with MQTT 5, a stable client id, persistent-client mode, and a 60-second session expiry. It subscribed to lab/session at QoS 1, received SUBACK, then disconnected. A separate publisher sent offline-one at QoS 1 while the subscriber was absent. The broker returned PUBACK to the publisher.

When lab-durable reconnected with the same identity and persistence settings, it received the queued PUBLISH before the new SUBACK appeared in the trace. The payload was offline-one. That order is useful evidence: the queued delivery came from state that existed before the repeated subscribe command completed.

The lesson is not that QoS 1 means offline delivery. Offline delivery occurred because the broker retained a session that included the subscription, the publisher used a matching topic and QoS, and the client returned before the session expired. Change any of those conditions and the result can change.

QoS 1 is not application exactly-once

MQTT QoS 1 is an at-least-once protocol flow. The sender retains a QoS 1 PUBLISH until it receives PUBACK. A reconnect or lost acknowledgment can cause redelivery. The receiving application therefore needs a policy for duplicates when repeating an effect would be harmful.

Use an application identifier when the business operation has identity. Store that identifier with the side effect in one atomic boundary where possible. A telemetry sample may tolerate duplication. A command that opens a valve, creates a charge, or advances a workflow often cannot.

The lab transcript shows a single delivery and PUBACK. It does not manufacture a duplicate or claim that Mosquitto will always deliver once. The standard permits the broader at-least-once behavior. A responsible test plan adds controlled connection loss around PUBLISH and PUBACK, then checks the consumer’s idempotency path.

Message expiry can override a live session

The second run created another persistent client, lab-expiry, with a 60-second session expiry and a QoS 1 subscription to lab/expiry. While that client was offline, a publisher sent expires-before-resume with a Message Expiry Interval of one second. The publisher received PUBACK. The run waited two seconds before reconnecting the subscriber.

The subscriber reconnected and received SUBACK, but no PUBLISH arrived. The client timed out with exit status 27. The session had not reached its 60-second expiry. The publication had reached its own one-second expiry.

This distinction should be explicit in an architecture review. Session expiry controls how long session state can remain after disconnect. Message expiry limits the lifetime of an individual application message. A durable session is not a promise to deliver data whose own lifetime has ended.

Expiry is useful when stale work is worse than missing work. It also creates an observability requirement. A publisher’s PUBACK proves that the broker accepted the QoS 1 transfer. It does not prove that an offline subscriber later consumed the message before expiry. If that outcome matters, measure it at the application boundary.

Clean Start changes the recovery story

After the queued-delivery test, lab-durable connected with Clean Start and a Session Expiry Interval of zero. The broker accepted the connection and subscription. This connection replaced the prior stored session instead of resuming it.

A client library that generates a new client id on every start has a similar operational effect: the broker cannot associate the new connection with the old session. A stable id with the wrong Clean Start setting can also discard state. Treat identity and session flags as deployed configuration. Log them at connection time without exposing credentials.

What to instrument

Broker and client telemetry should let you reconstruct the state transition. Record the client id, negotiated protocol version, Clean Start value, requested and granted QoS, Session Expiry Interval, reason codes, disconnect cause, and reconnect time. Track queued-message depth and expired-message counts when the broker exposes them.

At the consumer, record application message identity, receive time, completion status, and deduplication decision. Protocol acknowledgment and business completion are different events. Keeping both lets you distinguish transport recovery from application recovery.

Avoid a single “connected” dashboard. A green connection says nothing about whether the client resumed the intended session, whether queued data expired, or whether the application applied a redelivered command twice.

Failure-oriented test plan

Build tests around state transitions, not around a long-lived happy path.

  1. Establish a session with a pinned client id and known expiry. Confirm the granted subscription.
  2. Disconnect the subscriber. Publish at each supported QoS and capture publisher acknowledgments.
  3. Reconnect before session expiry. Confirm which messages arrive and whether the application applies them once.
  4. Repeat after session expiry and with Clean Start. Confirm that old state is absent.
  5. Add Message Expiry Interval values on both sides of the expected outage duration. Confirm that stale messages do not reach the consumer.
  6. Interrupt the connection around acknowledgment boundaries. Confirm duplicate handling and retained state.
  7. Restart the broker with the production persistence configuration. Confirm which state survives the process boundary.

Pin the broker image, configuration, client versions, and exact commands. Preserve failed runs. A timeout that was expected because a message expired is still evidence, but only when the transcript shows the wait, reconnect, and exit status.

Architecture decision

Use persistent MQTT sessions when the application needs subscriptions or eligible QoS messages to survive a temporary disconnect. Choose a session expiry that matches the realistic outage window and storage policy. Add message expiry when stale data should be dropped sooner. Design QoS 1 consumers for duplicates.

Do not describe the resulting system as “guaranteed delivery” without the conditions. State the client identity rule, session settings, supported QoS, message lifetime, broker persistence boundary, and application acknowledgment model. Those details are the guarantee.

Written by Context Systems. This piece was researched from the project’s public docs, release notes, and source. The way we write everything.

Your next piece can start with the product the same way.

See pricing