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
- Duplicate or doubled messages. You are creating the client outside
st.cache_resource, or you editedstart_mqttand Streamlit created a new cached client while the old thread keeps running. Restart thestreamlit runprocess after changing the MQTT setup. - Dashboard says "Connecting..." forever. Port 1883 may be blocked on your network. Switch to TLS on 8883 with
client.tls_set(), or test withmosquitto_sub -h broker.connectmq.com -p 1883 -i test-k7f3q2 -t 'connectmq/demo/k7f3q2/#' -v. - No data but connected. The prefix in
dashboard.pyandsimulator.pymust match exactly, including case. - Charts jump around. Timestamps come from the publisher. If devices have unsynchronised clocks, use the receive time instead (
time.time()insideon_message). - Slow page with many sensors. Pivoting thousands of rows every two seconds adds up. Reduce
MAX_POINTS, raiserun_every, or downsample withdf.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.tomland read them withst.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.



