IoT Tutorials

Paho MQTT Python Tutorial: Publish and Subscribe in 10 Minutes

Install paho-mqtt 2.x, build a working publisher and subscriber against broker.connectmq.com, learn the network loop, QoS, retained messages and Last Will, then switch to a private broker with credentials and TLS.

Python terminal running a paho mqtt python publisher and subscriber exchanging sensor messages
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Paho MQTT Python is the standard way to talk MQTT from Python, whether you are scripting a Raspberry Pi sensor, building a backend service or testing a device. In this tutorial you will install paho-mqtt 2.x, write a subscriber and a publisher that talk through broker.connectmq.com, and understand the network loop well enough to avoid the usual pitfalls. At the end you will switch the same code to a private broker with credentials and TLS.

What You Need

  • Python 3.8 or newer (python --version)
  • A terminal with outbound access to port 1883, or 8883 for TLS
  • Ten minutes

No account is needed for the first part. The free public MQTT broker at broker.connectmq.com accepts anonymous connections with any non-empty, unique client ID. Its public area is readable by anyone, so use a random topic prefix and never send real data there.

Install paho-mqtt

Create a virtual environment and install the library:

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install "paho-mqtt>=2.0"
python -c "import paho.mqtt; print(paho.mqtt.__version__)"

You should see a 2.x version. Version 2.0 changed how the client is constructed and how callbacks are called. Many tutorials online still show the 1.x style, which now fails or prints deprecation warnings. Everything here uses the current style; if you are migrating existing code, see the paho-mqtt 2.0 callback API version guide.

Step 1: A Subscriber

Save this as subscriber.py. Change k7f3q2 to your own random string:

import uuid

import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
PORT = 1883
TOPIC = "connectmq/demo/k7f3q2/#"


def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"Connection failed: {reason_code}")
        return
    print(f"Connected to {BROKER}")
    # Subscribe here so the subscription is restored after every reconnect.
    client.subscribe(TOPIC, qos=1)


def on_subscribe(client, userdata, mid, reason_code_list, properties):
    for rc in reason_code_list:
        if rc.is_failure:
            print(f"Subscription rejected: {rc}")
        else:
            print(f"Subscribed with QoS {rc.value}")


def on_message(client, userdata, msg):
    retained = " (retained)" if msg.retain else ""
    print(f"{msg.topic}{retained}: {msg.payload.decode('utf-8', errors='replace')}")


def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    print(f"Disconnected: {reason_code}")


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"sub-{uuid.uuid4().hex[:8]}")
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_message = on_message
client.on_disconnect = on_disconnect

client.connect(BROKER, PORT, keepalive=60)
client.loop_forever()

Run it with python subscriber.py. It prints "Connected" and "Subscribed with QoS 1", then waits.

Step 2: A Publisher

In a second terminal, save this as publisher.py:

import json
import random
import time
import uuid

import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
PORT = 1883
PREFIX = "connectmq/demo/k7f3q2"

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"pub-{uuid.uuid4().hex[:8]}")

# Last Will: published by the broker if this client disappears without disconnecting.
client.will_set(f"{PREFIX}/status/publisher", payload="offline", qos=1, retain=True)

client.connect(BROKER, PORT, keepalive=60)
client.loop_start()  # network loop in a background thread

# Retained status: new subscribers get it immediately.
client.publish(f"{PREFIX}/status/publisher", "online", qos=1, retain=True)

try:
    for i in range(10):
        payload = json.dumps({"seq": i, "temperature": round(random.uniform(20, 25), 2)})
        info = client.publish(f"{PREFIX}/sensors/room1", payload, qos=1)
        info.wait_for_publish(timeout=5)
        print(f"Sent #{i} (mid={info.mid}, published={info.is_published()})")
        time.sleep(1)
finally:
    client.publish(f"{PREFIX}/status/publisher", "offline", qos=1, retain=True).wait_for_publish(5)
    client.disconnect()
    client.loop_stop()

Run python publisher.py. The subscriber prints the status message and ten sensor readings, one per second.

Understanding the Paho MQTT Python Network Loop

The network loop is the part of paho that reads from and writes to the socket, sends keepalive pings and calls your callbacks. Nothing happens without it. You have three ways to run it:

Method Blocks? Use it when
loop_forever() Yes, until disconnect() The script only reacts to messages, such as a subscriber or logger
loop_start() / loop_stop() No, runs in a background thread Your main thread does other work, such as reading sensors
loop(timeout) One iteration per call You integrate with your own event loop

The most common beginner bug is calling publish() without any loop running and wondering why nothing arrives, or why QoS 1 messages are never acknowledged. Another is forgetting that callbacks run on the loop thread when you use loop_start(); if a callback touches data your main thread also uses, protect it with a lock or a queue.Queue.

loop_forever() and loop_start() both reconnect automatically after a dropped connection. That is why the subscriber subscribes inside on_connect: with a clean session, subscriptions are lost on reconnect and must be made again. The paho-mqtt reconnect guide goes deeper into backoff and offline buffering.

QoS, Retained Messages and Last Will in Practice

The publisher used three MQTT features worth understanding:

QoS 1 means the broker acknowledges each message, and paho resends it if the acknowledgment does not arrive. wait_for_publish() blocks until that acknowledgment, which is handy in scripts that exit right after publishing. QoS 0 is fire-and-forget and slightly cheaper; QoS 2 guarantees exactly-once delivery at the cost of an extra round trip. Our MQTT QoS levels explained article compares them in detail.

Retained messages are stored by the broker, one per topic, and sent immediately to anyone who subscribes later. They are ideal for state such as "online" or a device's current configuration. Restart the subscriber and notice that it receives the retained status straight away, marked "(retained)".

Last Will is registered at connect time. If the publisher crashes or loses its network, the broker publishes "offline" for it after the keepalive expires. Press Ctrl+C during the loop and you will see the clean "offline" from the finally block; kill the process with kill -9 instead and the broker sends the will.

Testing With Command-Line Tools

You can verify your Python code from the other side with the Mosquitto clients:

mosquitto_sub -h broker.connectmq.com -p 1883 -i cli-k7f3q2 -t 'connectmq/demo/k7f3q2/#' -v

The ConnectMQ portal offers the same view without installing anything: its in-browser MQTT client and live topic explorer show every topic under your prefix as messages arrive, which is useful when you are debugging on a machine where you cannot install Mosquitto. Keep in mind that the portal works with your private broker, while the CLI command above targets the public one.

If the CLI receives your messages but your Python subscriber does not, the problem is in your subscriber code; if neither receives them, look at the publisher or the network.

Common Errors and How to Fix Them

  1. ValueError: Unsupported callback API version or a TypeError about callback_api_version: you are using 1.x-style construction with paho-mqtt 2.x. Pass mqtt.CallbackAPIVersion.VERSION2 as the first argument.
  2. TypeError: on_connect() takes 4 positional arguments but 5 were given: your callback has the old signature. Use the version 2 signatures shown above.
  3. ConnectionRefusedError or timeouts: port 1883 is blocked on your network. Try TLS on 8883.
  4. Connected, then disconnected in a loop: another client is using the same client ID. Generate a unique one, as the examples do.
  5. Nothing received: check that topics match exactly, including case and the prefix, and that a loop is running.
  6. Messages dropped on the public broker: it allows 10 messages per second per client and 64 KB per message. Slow down or batch readings.

Moving to a Private Broker With Credentials and TLS

For real data, create your own broker. Sign up at app.connectmq.com, choose the free Serverless plan, create a broker, and copy the generated username, password and client ID. Then change the connection code:

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="YOUR_CLIENT_ID")
client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.tls_set()  # uses the system CA store and verifies the hostname
client.connect("broker.connectmq.com", 8883, keepalive=60)

Use the host name shown for your broker in the portal. ConnectMQ brokers use a Let's Encrypt certificate, which the default CA bundle on most systems already trusts. If you hit certificate errors, the Python MQTT TLS guide walks through each one. Keep credentials in environment variables rather than in source files.

Where to Go Next

Once publish and subscribe work, these guides build on the same client:

The official Eclipse Paho Python documentation is the reference for every method and option.

Get Started with ConnectMQ

ConnectMQ is a managed MQTT broker that supports MQTT 3.1.1 and 5.0 on TCP, TLS and WebSocket, with a portal that includes an in-browser MQTT client and a live topic explorer for checking what your Python code publishes. Experiment on the public broker, then run production traffic on your own cloud MQTT broker. Create a free account on the Serverless plan, no credit card required.

Frequently Asked Questions

Which version of paho-mqtt should I use?

Use the current 2.x release. It requires a callback_api_version argument when creating the client, and version 2 callbacks are the recommended choice for new code.

What is the difference between loop_start and loop_forever?

loop_forever() blocks the calling thread and runs the network loop until you disconnect. loop_start() runs the same loop in a background thread so your main code keeps running.

Does paho mqtt python support MQTT 5?

Yes. Pass protocol=mqtt.MQTTv5 when creating the client to use MQTT 5 features such as properties, reason codes and session expiry.

Do I need an account to try paho-mqtt with ConnectMQ?

No. broker.connectmq.com accepts anonymous clients on port 1883 for testing. Create a free account when you need a private broker with credentials.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.