Fundamentals

MQTT Persistent Sessions: Clean Session vs Clean Start

Understand MQTT persistent sessions: cleanSession in 3.1.1, Clean Start and Session Expiry in MQTT 5, the Session Present flag and how QoS 1/2 messages queue.

Updated · 7 min read

An MQTT session is the state a broker and client keep about each other: which topics the client subscribed to and which messages are still in flight. With a persistent session, that state survives a disconnect, so a device that drops off the network for a few minutes comes back to the same subscriptions and receives the messages it missed. This article explains how sessions work in MQTT 3.1.1 (cleanSession) and MQTT 5 (Clean Start plus Session Expiry Interval).

What a session contains

The spec defines the session state on each side. On the broker it includes:

  • The existence of the session itself, identified by the client ID.
  • The client's subscriptions (topic filters and their QoS).
  • QoS 1 and QoS 2 messages sent to the client but not yet fully acknowledged.
  • QoS 1 and QoS 2 messages waiting to be sent to the client — the "offline queue".
  • QoS 2 messages received from the client but not yet fully acknowledged.
  • Optionally, QoS 0 messages waiting to be sent (brokers may choose to queue these, but usually do not).

The client keeps its own half: QoS 1 and 2 messages it sent but that are not fully acknowledged, and QoS 2 messages it received but has not completed. This is why the QoS levels only deliver their guarantees across reconnects when a persistent session is used.

MQTT 3.1.1: the cleanSession flag

In MQTT 3.1.1 a single bit in the CONNECT packet decides everything:

cleanSessionOn connectOn disconnect
1 (true)Any existing session for this client ID is discarded; a new one startsThe session is deleted
0 (false)An existing session is resumed; otherwise a new one is createdThe session is kept, and QoS 1/2 messages for its subscriptions are queued

A persistent session in 3.1.1 never expires on its own according to the protocol. Brokers add their own limits, such as Mosquitto's persistent_client_expiration and max_queued_messages settings, otherwise abandoned client IDs would accumulate forever. A zero-length client ID is only allowed with cleanSession=1, because a session that cannot be identified cannot be resumed.

MQTT 5: Clean Start and Session Expiry Interval

MQTT 5 splits the old flag into two independent controls:

  • Clean Start (a flag) — whether to discard any existing session when connecting.
  • Session Expiry Interval (a CONNECT property, in seconds) — how long the broker keeps the session after the network connection closes. Absent or 0 means the session ends with the connection; 0xFFFFFFFF means it never expires.

That gives you combinations 3.1.1 could not express, such as "always start fresh, but keep my session for one hour if I drop off" (Clean Start = 1, expiry = 3600). The client can also change the expiry in its DISCONNECT packet, as long as it did not connect with an expiry of 0.

MQTT 3.1.1Equivalent MQTT 5 settings
cleanSession = 1Clean Start = 1, Session Expiry Interval = 0
cleanSession = 0Clean Start = 0, Session Expiry Interval = 0xFFFFFFFF

MQTT 5 brokers may also shorten the requested expiry and announce the real value with a Session Expiry Interval property in CONNACK. Combine it with the per-message Message Expiry Interval so queued commands do not get delivered hours after they stopped being relevant. More differences are covered in MQTT 5 vs 3.1.1.

The Session Present flag

The broker's CONNACK carries a Session Present flag that tells the client what actually happened:

  • 1 — the broker found and resumed a stored session. Subscriptions are still active; you do not need to resubscribe.
  • 0 — no session was resumed (Clean Start was set, it expired, or the broker lost it). The client must subscribe again.

Robust clients check this flag rather than assuming. The safest simple pattern is to resubscribe whenever Session Present is 0; resubscribing when it is 1 is allowed but replaces the existing subscriptions and may cause retained messages to be resent.

Examples

With the Mosquitto CLI, -c disables clean session and -i sets a fixed client ID — both are required for a persistent session:

Persistent subscriber with mosquitto_sub
# 1. Create the session, then press Ctrl+C
mosquitto_sub -h broker.hivemq.com -i sensor-42-sub -c -q 1 -t 'testmqtt/sess-demo/#'

# 2. While it is offline, publish with QoS 1
mosquitto_pub -h broker.hivemq.com -q 1 -t 'testmqtt/sess-demo/cmd' -m 'reboot'

# 3. Reconnect with the same ID: the queued message is delivered
mosquitto_sub -h broker.hivemq.com -i sensor-42-sub -c -q 1 -t 'testmqtt/sess-demo/#'
MQTT.js — MQTT 5 session that survives one hour
import mqtt from 'mqtt';

const client = mqtt.connect('wss://broker.hivemq.com:8884/mqtt', {
  protocolVersion: 5,
  clientId: 'sensor-42-sub',
  clean: false, // Clean Start = 0
  properties: { sessionExpiryInterval: 3600 },
});

client.on('connect', (connack) => {
  if (!connack.sessionPresent) {
    client.subscribe('testmqtt/sess-demo/#', { qos: 1 });
  }
});
Paho MQTT (Python) — MQTT 3.1.1
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, reason_code, properties):
    if not flags.session_present:
        client.subscribe("testmqtt/sess-demo/#", qos=1)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id="sensor-42-sub", clean_session=False)
client.on_connect = on_connect
client.connect("broker.hivemq.com", 1883)
client.loop_forever()

You can generate equivalent code for other languages with the MQTT code generator, and use the client ID generator to create stable, unique IDs. To publish the test messages from your browser, use the online client:

Try it in the online MQTT client

How long should a session live?

With MQTT 5 you have to pick a Session Expiry Interval, and the right value depends on how long the client can realistically be away and still care about what it missed:

  • Mobile apps and dashboards — minutes. Long enough to survive a tunnel or a network switch, short enough that the broker is not holding state for users who closed the app.
  • Field devices on unreliable links — hours to a day, matched to typical outage lengths.
  • Backend consumers — long enough to cover a deployment or restart window, so no message is lost while a new version starts.

Sessions also interact with the Last Will. In MQTT 5 the will is published when the Will Delay Interval passes or the session ends, whichever comes first. A short will delay with a longer session lets a device reconnect quietly after a brief glitch without triggering an "offline" alert.

Common pitfalls

  • Random client IDs. Many libraries generate a random ID on each start, which silently creates a new session every time. Persistent sessions require a stable ID.
  • QoS 0 somewhere in the chain. The delivered QoS is the lower of the publish and subscription QoS. If either is 0, messages are not queued for offline clients.
  • Two clients with the same ID. The broker disconnects the older one when the newer one connects, and the two fight over the session. See MQTT connection errors.
  • Unbounded queues. A device that is offline for days can accumulate a large backlog. Use session and message expiry, and broker queue limits, to keep this under control.
  • Broker restarts. Sessions survive a broker restart only if the broker persists them to disk (for Mosquitto, persistence true).

When to use persistent sessions

Use them for devices that must not miss commands or configuration updates, and for backend consumers that must process every QoS 1 message. Prefer clean sessions for short-lived tools, dashboards that only care about the latest values (retained messages cover that), and anything that connects with throwaway client IDs. On flaky networks, pair a persistent session with a sensible keep alive interval so disconnects are detected quickly and the queue takes over.

Frequently asked questions

What does clean session false mean in MQTT?

With cleanSession set to false in MQTT 3.1.1, the broker keeps the client session after it disconnects: its subscriptions and any QoS 1 and QoS 2 messages that arrive while it is offline. When the client reconnects with the same client ID, it resumes that session and receives the queued messages.

Why am I not receiving offline messages with a persistent session?

Check four things: the client reconnects with exactly the same client ID, clean session is false (or Clean Start is false with a non-zero Session Expiry Interval in MQTT 5), both the subscription and the publish use QoS 1 or 2, and the broker has not hit its queue limit or session expiry.

What is the difference between Clean Session and Clean Start?

In MQTT 3.1.1, cleanSession controls both whether an old session is discarded on connect and whether the new one survives disconnection. MQTT 5 splits these: Clean Start only controls discarding the old session, and the Session Expiry Interval controls how long the session survives after disconnecting.