IoT Tutorials

Streamlit MQTT Dashboard in Pure Python: Live Sensor Charts in 100 Lines

Build a live Streamlit dashboard fed by a background paho-mqtt subscriber connected to broker.connectmq.com, with a thread-safe buffer, auto-refreshing charts and a simulator to feed it.

Streamlit mqtt dashboard showing live temperature and humidity line charts from IoT sensors
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

A Streamlit MQTT dashboard lets you turn live sensor messages into charts and metrics without writing a line of HTML or JavaScript. In this tutorial you will build one in pure Python: a background paho-mqtt subscriber connected to broker.connectmq.com, a thread-safe buffer, and a page that refreshes itself every two seconds. You will also build a small simulator so the dashboard has data from the first minute.

Why Streamlit and MQTT Need a Little Glue

Streamlit has an unusual execution model. Every time a user interacts with the page, or a refresh is triggered, Streamlit reruns your whole script from top to bottom. That is great for simple data apps, but MQTT clients are long-lived: they hold a TCP connection, run a network loop and fire callbacks whenever a message arrives.

If you naively create an MQTT client at the top of the script, every rerun creates another connection. Within a minute you have dozens of clients, duplicate messages and, on the public broker, connections being dropped for reusing client IDs. The fix is to create the client exactly once per server process and let Streamlit read from a shared buffer on each rerun. Streamlit provides the right tool for that: st.cache_resource.

The architecture looks like this:

Component Runs where Responsibility
paho-mqtt client Background network thread (loop_start) Keeps the connection, receives messages
deque buffer + lock Shared memory, created once Holds the most recent N readings
Streamlit script Rerun on every refresh Copies the buffer, builds a DataFrame, draws charts
st.fragment(run_every=...) Streamlit scheduler Reruns only the chart section every few seconds

Prerequisites

You need Python 3.9 or newer and three packages:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "streamlit>=1.37" "paho-mqtt>=2.0" pandas

Streamlit 1.37 introduced st.fragment with the run_every parameter, which this tutorial uses for auto-refresh. paho-mqtt 2.x requires the explicit callback API version shown below; if you are coming from older code, read the paho-mqtt 2.0 callback API guide first.

Pick a random topic prefix so your data does not mix with anyone else's on the public MQTT test broker. This tutorial uses connectmq/demo/k7f3q2; replace k7f3q2 with your own random string in both files.

Step 1: A Sensor Simulator

Before building the dashboard, create something that publishes data. Save this as simulator.py:

import json
import math
import random
import time
import uuid

import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
PREFIX = "connectmq/demo/k7f3q2"
SENSORS = ["line1", "line2", "line3"]

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"sim-{uuid.uuid4().hex[:8]}")
client.connect(BROKER, 1883, keepalive=60)
client.loop_start()

t = 0
try:
    while True:
        for sensor in SENSORS:
            reading = {
                "sensor": sensor,
                "temperature": round(22 + 3 * math.sin(t / 20) + random.uniform(-0.5, 0.5), 2),
                "humidity": round(45 + 5 * math.cos(t / 30) + random.uniform(-1, 1), 2),
                "ts": time.time(),
            }
            client.publish(f"{PREFIX}/sensors/{sensor}", json.dumps(reading), qos=0)
        t += 1
        time.sleep(1)
except KeyboardInterrupt:
    pass
finally:
    client.loop_stop()
    client.disconnect()

It publishes three messages per second, well below the public broker's limit of 10 messages per second per client. Run it in its own terminal with python simulator.py and leave it running.

Step 2: The Streamlit MQTT Dashboard

Save this as dashboard.py:

import json
import threading
import time
import uuid
from collections import deque

import pandas as pd
import paho.mqtt.client as mqtt
import streamlit as st

BROKER = "broker.connectmq.com"
PORT = 1883
PREFIX = "connectmq/demo/k7f3q2"
TOPIC = f"{PREFIX}/sensors/+"
MAX_POINTS = 600


@st.cache_resource
def start_mqtt():
    """Create one MQTT client per Streamlit server process."""
    state = {
        "buffer": deque(maxlen=MAX_POINTS),
        "lock": threading.Lock(),
        "connected": False,
        "received": 0,
    }

    def on_connect(client, userdata, flags, reason_code, properties):
        if reason_code.is_failure:
            return
        state["connected"] = True
        client.subscribe(TOPIC, qos=0)

    def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
        state["connected"] = False

    def on_message(client, userdata, msg):
        try:
            data = json.loads(msg.payload)
            row = {
                "time": pd.to_datetime(data.get("ts", time.time()), unit="s"),
                "sensor": data.get("sensor", msg.topic.rsplit("/", 1)[-1]),
                "temperature": float(data["temperature"]),
                "humidity": float(data["humidity"]),
            }
        except (ValueError, KeyError, TypeError):
            return  # ignore malformed payloads
        with state["lock"]:
            state["buffer"].append(row)
            state["received"] += 1

    client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id=f"streamlit-{uuid.uuid4().hex[:8]}",
    )
    # For a private ConnectMQ broker:
    # client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
    # client.tls_set()  and use port 8883
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_message = on_message
    client.reconnect_delay_set(min_delay=1, max_delay=30)
    client.connect_async(BROKER, PORT, keepalive=60)
    client.loop_start()
    return state


st.set_page_config(page_title="MQTT Sensor Dashboard", layout="wide")
st.title("Live MQTT Sensor Dashboard")
st.caption(f"Subscribed to `{TOPIC}` on {BROKER}")

state = start_mqtt()


@st.fragment(run_every=2)
def live_view():
    with state["lock"]:
        rows = list(state["buffer"])
        received = state["received"]

    status = "Connected" if state["connected"] else "Connecting..."
    c1, c2, c3 = st.columns(3)
    c1.metric("Broker status", status)
    c2.metric("Messages received", received)
    c3.metric("Points in buffer", len(rows))

    if not rows:
        st.info("Waiting for data. Is simulator.py running with the same prefix?")
        return

    df = pd.DataFrame(rows)
    latest = df.groupby("sensor").last()

    cols = st.columns(len(latest))
    for col, (sensor, r) in zip(cols, latest.iterrows()):
        col.metric(f"{sensor} temperature", f"{r['temperature']:.1f} °C")

    temp = df.pivot_table(index="time", columns="sensor", values="temperature")
    hum = df.pivot_table(index="time", columns="sensor", values="humidity")

    left, right = st.columns(2)
    left.subheader("Temperature")
    left.line_chart(temp)
    right.subheader("Humidity")
    right.line_chart(hum)


live_view()

Start it with:

streamlit run dashboard.py

Streamlit opens http://localhost:8501. Within a couple of seconds the status shows Connected, the counters start climbing and three lines appear on each chart.

How the Dashboard Works

One client, many reruns

st.cache_resource runs start_mqtt() the first time and returns the same object on every later call, across reruns and across browser sessions on the same server. That is exactly the lifetime an MQTT connection needs. Because the cached object is shared by every viewer, the dashboard opens one broker connection no matter how many people are looking at it, which matters on plans with a connection limit.

Thread safety

paho's loop_start() runs the network loop in a background thread, so on_message executes on a different thread from your Streamlit script. Appending to a deque is atomic in CPython, but copying it while another thread appends can raise "deque mutated during iteration". The lock makes the copy and the append mutually exclusive, and the lock is held only for microseconds, so it never slows the network thread.

The maxlen on the deque is your memory guard. With 600 points the dashboard shows roughly the last 200 seconds of three sensors, and memory stays flat no matter how long it runs.

Partial reruns with fragments

@st.fragment(run_every=2) tells Streamlit to rerun only that function every two seconds instead of the whole script. The title and caption stay put, the charts update, and the page does not flicker. On older Streamlit versions you would need a third-party auto-refresh component or a time.sleep() plus st.rerun() loop, both of which are clumsier.

Reconnects

connect_async() plus loop_start() means the dashboard starts even if the broker is briefly unreachable, and paho retries in the background with the delays set by reconnect_delay_set. Subscribing inside on_connect is important: after a reconnect with a clean session, subscriptions are gone, and on_connect is where they get restored. The paho-mqtt reconnect guide covers this pattern in more depth.

Common Problems and Fixes

  1. Duplicate or doubled messages. You are creating the client outside st.cache_resource, or you edited start_mqtt and Streamlit created a new cached client while the old thread keeps running. Restart the streamlit run process after changing the MQTT setup.
  2. Dashboard says "Connecting..." forever. Port 1883 may be blocked on your network. Switch to TLS on 8883 with client.tls_set(), or test with mosquitto_sub -h broker.connectmq.com -p 1883 -i test-k7f3q2 -t 'connectmq/demo/k7f3q2/#' -v.
  3. No data but connected. The prefix in dashboard.py and simulator.py must match exactly, including case.
  4. Charts jump around. Timestamps come from the publisher. If devices have unsynchronised clocks, use the receive time instead (time.time() inside on_message).
  5. Slow page with many sensors. Pivoting thousands of rows every two seconds adds up. Reduce MAX_POINTS, raise run_every, or downsample with df.resample() before charting.

Taking It to Production

The public broker is ideal for experiments, but everything in its public area is readable by anyone. For a dashboard that shows real plant or building data:

  • Create a private broker on ConnectMQ and use the generated credentials with TLS on port 8883. The Python MQTT TLS guide shows the exact tls_set() configuration.
  • Keep secrets in .streamlit/secrets.toml and read them with st.secrets["mqtt"]["username"] instead of hard-coding them.
  • If you need history beyond the in-memory buffer, run a separate subscriber that writes to a database and have the dashboard query it. The MQTT to SQLite in Python tutorial is a lightweight starting point, and the MQTT and pandas guide helps with analysis.

Streamlit is a Python tool, but it sits in the same family as browser dashboards. If you prefer a pure front-end approach, the MQTT JavaScript browser client connects straight from the page over WebSocket, and the MQTT client libraries overview compares the main options in each language. The Streamlit documentation covers fragments, caching and deployment in detail.

Run Your Streamlit Dashboards on ConnectMQ

ConnectMQ is a fully managed MQTT broker: you get TLS on 8883, generated credentials, and a portal with a live topic explorer and in-browser client to check what your sensors publish before you chart it. Start with broker.connectmq.com, then move to your own cloud MQTT broker. Sign up for the free Serverless plan, no credit card required.

Frequently Asked Questions

Can Streamlit subscribe to MQTT directly?

Streamlit has no built-in MQTT support, but any Python MQTT client works. The reliable pattern is a paho-mqtt client created once with st.cache_resource and running its network loop in a background thread.

How often should the dashboard refresh?

One to five seconds suits most sensor dashboards. Faster refreshes cost CPU on the Streamlit server without making slow-changing values more useful.

Why do I get each message several times?

Multiple MQTT clients are running, usually because the client was created outside st.cache_resource or an old cached client survived a code change. Restart the Streamlit process so only one client exists.

Can many users view the same Streamlit MQTT dashboard?

Yes. The cached client is shared by every session on the server, so ten viewers still use a single broker connection and read from the same buffer.

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.