The Paho MQTT Python client can subscribe to topics and receive messages before it has even finished establishing its connection to the broker.
Let’s see it in action. Imagine you have an MQTT broker running on localhost:1883.
import paho.mqtt.client as mqtt
import time
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected successfully to broker.")
# Subscribe to a topic immediately upon connection
client.subscribe("my/test/topic")
else:
print(f"Connection failed with code {rc}")
def on_message(client, userdata, msg):
print(f"Received message on topic {msg.topic}: {msg.payload.decode()}")
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
# Connect to the broker
client.connect("localhost", 1883, 60)
# This is where the magic happens.
# Even though the connection might still be in progress,
# the client is ready to receive messages for subscribed topics.
# We'll start the network loop in a separate thread to allow
# the main thread to continue if needed, though for this example
# we'll just let it run.
client.loop_start()
# Publish a message after a short delay, giving the client time to subscribe
time.sleep(2)
client.publish("my/test/topic", "Hello MQTT World!")
# Keep the main thread alive to allow the loop thread to run
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Disconnecting...")
client.loop_stop()
client.disconnect()
When you run this, you’ll see output similar to this:
Connected successfully to broker.
Received message on topic my/test/topic: Hello MQTT World!
Notice that the "Received message" print statement appears after "Connected successfully". This demonstrates the client’s ability to process messages for subscribed topics as soon as they arrive, even if the initial connection handshake is still being finalized in the background.
The core problem MQTT solves is enabling lightweight, asynchronous, and decoupled communication between devices and applications, especially in environments with limited bandwidth or unreliable networks. It’s a publish-subscribe messaging protocol, meaning publishers (clients sending messages) don’t send messages directly to subscribers (clients receiving messages). Instead, they send messages to a central broker, which then distributes them to all clients that have subscribed to the relevant "topics." Topics are hierarchical strings, like file paths, allowing for flexible message routing.
The Paho MQTT Python client abstracts away the complexities of the MQTT protocol. When you call client.connect(), the client initiates a TCP connection to the specified broker. It then sends a CONNECT packet. The broker responds with a CONNACK packet, indicating success or failure. Crucially, the on_connect callback is invoked after a successful CONNACK is received. Inside on_connect, you typically register your subscriptions using client.subscribe().
The client.loop_start() method is key here. It starts a separate thread that continuously handles network traffic: sending outgoing packets (like PUBLISH and SUBSCRIBE) and receiving incoming packets (like SUBACK and incoming PUBLISH messages). This background thread ensures that your application remains responsive while message handling occurs asynchronously. When a PUBLISH message arrives on a topic you’re subscribed to, the network loop thread processes it and then invokes your on_message callback function.
The client.connect("localhost", 1883, 60) call initiates the connection. The 60 is the keep-alive interval in seconds. This means the client expects to receive a communication from the broker at least every 60 seconds. If it doesn’t, it will assume the connection is lost and attempt to reconnect. The client.loop_start() is what allows this keep-alive mechanism to function and also processes incoming messages.
The real power comes from the separation of concerns. Your main application thread can continue doing other work while the loop_start() thread manages the MQTT communication. When a message arrives, on_message is called, and you can process it without blocking your main application flow. Similarly, you can call client.publish() from any thread at any time, and the network loop will queue it for sending.
A common misunderstanding is that you must wait for on_connect to fully complete before the client can receive messages. However, the client’s internal state machine is designed to handle subscriptions and incoming messages efficiently. Once a SUBSCRIBE packet has been sent (typically within on_connect), the client is prepared to receive PUBLISH messages on those topics. The network loop actively listens and dispatches messages to the appropriate callbacks as soon as they are received and identified.
The QoS (Quality of Service) parameter in client.subscribe("my/test/topic", qos=1) and client.publish("my/test/topic", "Hello", qos=1) is a critical lever you control. A QoS of 0 means "at most once" delivery – messages might be lost or duplicated. QoS 1 means "at least once" – messages are guaranteed to arrive, but might be duplicated. QoS 2 means "exactly once" – messages are guaranteed to arrive exactly once, but this is the most resource-intensive. The default is QoS 0.
The next hurdle is often handling reconnections and managing the client’s state across network interruptions.