Your idea is safe; NDA signed before discussion
Homeโ€บBlogโ€บESP32 with MQTT: Building Real-Time IoT Dashboards
IoT Architecture Deep Dive

ESP32 with MQTT:
Building Real-Time
IoT Dashboards

The complete guide to connecting ESP32 firmware to an MQTT broker, implementing QoS levels, securing with TLS, and streaming live data to a React dashboard โ€” built from a real irrigation monitor deployment.

  • Full ESP32 โ†’ Broker โ†’ React architecture
  • QoS 0, 1 & 2 with real use-cases
  • TLS 1.2 on WiFiClientSecure โ€” production config
  • Smart topic design that scales to 1,000s of devices

๐Ÿ“ก Full Stack Architecture

๐Ÿ”ง
ESP32 Device
Reads sensors ยท publishes over WiFi
โ†“ MQTT Publish over TLS (port 8883)
๐Ÿ“ก
MQTT Broker
Mosquitto ยท HiveMQ ยท AWS IoT Core
โ†“ Subscribe via WebSocket (WSS)
โš›๏ธ
React Dashboard
MQTT.js ยท Recharts ยท live updates
<200ms
End-to-End Latency
QoS 0โ€“2
Delivery Guarantees
TLS 1.2
Transport Security
1,000s
Concurrent Devices
Protocol Fundamentals

Why MQTT Is the Right Protocol for IoT

Most IoT developers default to HTTP out of habit. It works โ€” but heavyweight headers and polling overhead constrain battery-powered field devices. MQTT was designed from scratch for exactly this problem.

โšก Lightweight by Design

2-byte minimum header. ESP32 publishes 500 messages per hour on a single LiPo charge โ€” impossible with REST/HTTP polling cycles.

๐Ÿ”— Persistent Connections

One TCP connection stays open. No handshake overhead per message โ€” ideal for high-frequency sensor telemetry where every millisecond matters.

๐Ÿ“ถ Offline Tolerance

Retained messages and persistent sessions mean the broker buffers data when the ESP32 loses signal and replays on reconnect โ€” no data loss.

โš–๏ธ Three QoS Tiers

Fire-and-forget, at-least-once, or exactly-once delivery โ€” matched per-topic to the criticality of each data stream.

MQTT is broker-mediated: the ESP32 publishes sensor readings to a named topic; the React dashboard subscribes and receives data instantly. This decoupling makes scaling from one device to thousands trivially easy โ€” without changing firmware or frontend code.

For the irrigation monitor we built for agricultural clients, ESP32 nodes transmitted soil moisture, temperature, and pump status every 30 seconds to a Mosquitto broker, with a React dashboard surfacing live readings. Latency from sensor read to dashboard update consistently measured under 200ms โ€” unachievable with polling.

System Design

Full Stack Architecture:
ESP32 โ†’ Broker โ†’ React

Three distinct layers, each independently scalable. Understanding the boundary between them separates hobby projects from production systems.

๐Ÿ”ง
ESP32 Device Layer

Firmware reads sensors via ADC / IยฒC / SPI. PubSubClient handles MQTT. WiFiClientSecure wraps TLS. Reconnect logic with exponential backoff prevents broker floods on WiFi recovery.

๐Ÿ“ก
MQTT Broker

Routes published messages to subscribers. Enforces authentication and ACL. Supports WebSocket listeners for browser clients. Retains last-known-good messages per topic.

โš›๏ธ
React Dashboard

MQTT.js connects over WebSocket (WSS). Custom useMqtt hook streams messages into React state. Recharts renders live time-series charts with sub-second refresh.

โœ“
Retained messages โ€” dashboard shows last-known state even before the next 30-second publish cycle
โœ“
Last Will Testament โ€” broker publishes "offline" status if device disconnects unexpectedly
โœ“
Exponential backoff โ€” staggered reconnect prevents thundering herd when 50 nodes recover simultaneously
โœ“
WebSocket bridge โ€” browsers can't open raw TCP; broker's WS listener bridges to MQTT.js
โœ“
Per-device credentials โ€” a compromised device can be revoked without restarting the full fleet
โœ“
Zero-config new subscribers โ€” add SMS alerter or DB writer without touching a single device's firmware
Firmware Implementation

ESP32 MQTT Client: The Firmware Side

Two libraries do the heavy lifting: PubSubClient for MQTT and WiFiClientSecure for TLS. Here's the production pattern we use across all ESP32 deployments.

irrigation_mqtt.ino
// ESP32 MQTT client with TLS โ€” DigitalMonk production pattern
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* MQTT_BROKER = "mqtt.yourbroker.io";
const int   MQTT_PORT   = 8883;  // TLS port

const char* ROOT_CA = R"(
-----BEGIN CERTIFICATE-----
... your broker CA cert here ...
-----END CERTIFICATE-----
)";

WiFiClientSecure wifiClient;
PubSubClient     mqttClient(wifiClient);

const char* PUB_TOPIC = "farm/01/zone/03/soil";
const char* CMD_TOPIC = "farm/01/zone/03/pump/cmd";

void mqttCallback(char* topic, byte* payload, unsigned int len) {
  String msg = String((char*)payload).substring(0, len);
  if (msg == "ON")  digitalWrite(PUMP_PIN, HIGH);
  if (msg == "OFF") digitalWrite(PUMP_PIN, LOW);
}

void reconnect() {
  int delay_ms = 1000;
  while (!mqttClient.connected()) {
    if (mqttClient.connect("esp32_zone03", "user", "pass")) {
      mqttClient.subscribe(CMD_TOPIC, 1);
    } else {
      delay(delay_ms);
      delay_ms = min(delay_ms * 2, 30000);  // exponential backoff
    }
  }
}

void publishSensorData() {
  StaticJsonDocument<128> doc;
  doc["moisture"] = readSoilMoisture();
  doc["temp_c"]   = readTemperature();
  doc["pump_on"]  = pumpActive;
  doc["ts"]       = millis();
  char buf[128]; serializeJson(doc, buf);
  mqttClient.publish(PUB_TOPIC, buf, true);  // retained=true
}

void setup() {
  wifiClient.setCACert(ROOT_CA);
  mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
  mqttClient.setCallback(mqttCallback);
  mqttClient.setKeepAlive(60);
}

void loop() {
  if (!mqttClient.connected()) reconnect();
  mqttClient.loop();
  static unsigned long last = 0;
  if (millis() - last > 30000) { publishSensorData(); last = millis(); }
}

The retained=true flag on publish stores the last reading on the broker. Any new subscriber โ€” including the React dashboard on first load โ€” immediately receives current state without waiting for the next publish cycle.

The exponential backoff in reconnect() is critical at fleet scale. Without it, 50 nodes recovering simultaneously create a thundering herd that overwhelms the broker. Backoff staggers reconnection naturally, capping at 30 seconds.

Delivery Guarantees

Understanding QoS Levels โ€” And Choosing the Right One

QoS is MQTT's delivery contract. Not all sensor data deserves the same treatment โ€” getting this wrong either wastes bandwidth or silently drops critical messages.

0
QoS 0 โ€” At Most Once
Fire and Forget

Published once, no acknowledgement, no retry. If the network drops, the message is lost. Lowest latency and overhead.

  • โœ… Best for high-frequency telemetry
  • โœ… Lowest overhead
  • โŒ No delivery confirmation
  • Use for: temperature reads, heartbeat pings
1
QoS 1 โ€” At Least Once
Guaranteed Delivery

Publisher retries until it receives a PUBACK. May produce duplicate messages โ€” subscribers must handle idempotency.

  • โœ… Guaranteed at least one delivery
  • โœ… Suitable for most IoT sensor data
  • โŒ Possible duplicates
  • Use for: soil moisture alerts, pump commands
2
QoS 2 โ€” Exactly Once
Highest Reliability

Four-part handshake guarantees exactly one delivery. Highest overhead โ€” reserve for when duplicates cause real consequences.

  • โœ… Zero duplicates, zero loss
  • โœ… Essential for safety-critical events
  • โŒ Significant latency overhead
  • Use for: billing events, valve open/close

In the irrigation monitor: routine readings (moisture, temperature) at QoS 0 โ€” data refreshes every 30 seconds anyway. Pump commands at QoS 1 โ€” we need confidence the command arrived, and duplicates are handled gracefully in firmware. Water usage billing events used QoS 2 โ€” duplicates there would cause double-charging.

Information Architecture

Topic Design: The Taxonomy That Scales

MQTT topics are free-form strings โ€” teams either design them deliberately or end up with an unmaintainable mess six months later.

Topic PathDirectionQoSRetainedPurpose
farm/{id}/zone/{z}/soilPUBLISH0โœ…Soil moisture + temperature JSON, every 30s
farm/{id}/zone/{z}/pump/cmdCOMMAND1โŒPump ON/OFF from dashboard to device
farm/{id}/zone/{z}/pump/statePUBLISH1โœ…Confirmed pump state reported back by ESP32
farm/{id}/zone/{z}/statusPUBLISH1โœ…Device heartbeat + Last Will Testament
farm/{id}/alertsPUBLISH2โŒThreshold breach alerts for SMS / email
farm/{id}/ota/updateCOMMAND2โŒOTA firmware URL pushed to device fleet

The pump/cmd and pump/state split is deliberate โ€” commands flow down, state flows up. The wildcard + matches one level; # matches all remaining. Subscribe to farm/# once to capture everything for a time-series database writer โ€” no firmware changes when new zones are added.

Security Layer

TLS on ESP32: Non-Negotiable in Production

Unencrypted MQTT on port 1883 exposes sensor data, pump commands, and credentials to anyone on the network path.

1

Obtain Your Broker's Root CA Certificate

Cloud brokers (HiveMQ Cloud, AWS IoT Core) provide a root CA. Self-hosted Mosquitto requires OpenSSL. Embed it in ESP32 flash โ€” it verifies the broker's identity and prevents man-in-the-middle attacks.

2

Choose Full Validation vs Insecure Mode

For development, wifiClient.setInsecure() skips cert verification โ€” fast to set up, never for production. For production, pass the CA cert via setCACert() and optionally add client certs for mutual TLS (mTLS).

3

Set MQTT Port to 8883

Port 1883 is plaintext TCP. Port 8883 is the IANA-assigned port for MQTT over TLS. Your broker must have TLS listening on 8883, and mqttClient.setServer() must reference it accordingly.

4

Use Unique Credentials Per Device

Every ESP32 authenticates with a unique client ID, username, and password โ€” or ideally a per-device X.509 certificate. This lets you revoke a single compromised device without disrupting the fleet.

Frontend Implementation

The React Dashboard: MQTT.js Meets Live Data

Browsers cannot open raw TCP sockets, so the broker exposes a WebSocket endpoint on port 8084 (WSS for TLS). MQTT.js handles the rest.

dashboard.irrigation.io/farm/01
๐ŸŒฑ Irrigation Monitor โ€” Farm 01
MQTT Connected ยท 3 Zones Live
๐Ÿ’ง
68%
Zone 01 Moisture
๐ŸŒก๏ธ
24ยฐC
Ambient Temp
โš™๏ธ
ON
Pump Status
๐Ÿ“ถ
3/3
Devices Online
Moisture History โ€” Zone 01 (last 60 min)
useMqtt.js โ€” React custom hook
import { useState, useEffect, useRef } from 'react';
import mqtt from 'mqtt';

export function useMqtt(brokerUrl, topics) {
  const [messages, setMessages] = useState({});
  const [status, setStatus] = useState('connecting');
  const clientRef = useRef(null);

  useEffect(() => {
    const client = mqtt.connect(brokerUrl, {
      username: process.env.REACT_APP_MQTT_USER,
      password: process.env.REACT_APP_MQTT_PASS,
      rejectUnauthorized: true,
      reconnectPeriod: 3000,
    });
    client.on('connect', () => {
      setStatus('connected');
      topics.forEach(t => client.subscribe(t, { qos: 1 }));
    });
    client.on('message', (topic, payload) => {
      try {
        const data = JSON.parse(payload.toString());
        setMessages(prev => ({ ...prev, [topic]: { ...data, receivedAt: new Date() } }));
      } catch (e) {}
    });
    client.on('offline', () => setStatus('reconnecting'));
    clientRef.current = client;
    return () => client.end();
  }, [brokerUrl]);

  return { messages, status };
}

// Usage โ€” wildcard catches all zones automatically
const { messages } = useMqtt('wss://broker.io:8084/mqtt', ['farm/01/+/soil']);
const moisture = messages['farm/01/zone/03/soil']?.moisture ?? '--';

The wildcard subscription farm/01/+/soil captures data from all zones in one subscribe call. New zones appear automatically as their topics start publishing โ€” the dashboard never changes when new hardware is added in the field.

Infrastructure Choice

Choosing Your MQTT Broker

The broker is the only component with real availability requirements โ€” extended downtime means data loss. Choose based on your operational context.

๐ŸฆŸ
Eclipse Mosquitto

Open-source, self-hosted on a $5/month VPS. Full control over config, persistence, and TLS. Ideal for cost-sensitive projects or data sovereignty. Requires ops overhead.

โญ RECOMMENDED
๐Ÿ
HiveMQ Cloud

Managed cloud broker with generous free tier (100 connections). Native WebSocket support. TLS out of the box. Zero ops overhead. Our default for greenfield projects under 1,000 devices.

โ˜๏ธ
AWS IoT Core

Enterprise-grade managed broker. Per-message billing. Deep AWS integration โ€” Lambda triggers, DynamoDB rules, Kinesis streams. Best when your stack already lives in AWS.

๐Ÿ“ Real-World Project

Irrigation Monitor: MQTT in Production

ESP32 nodes across field zones publishing soil moisture, temperature, and pump status every 30 seconds to Mosquitto. A React dashboard consumed live topic streams via WebSocket. Automation logic subscribed on the same broker, triggering pump commands the moment moisture dipped below threshold โ€” no cloud round-trip, no polling delay.

40%
Water Saved
<200ms
Sensor to Dashboard
ESP32
Hardware Core
MQTT
Messaging Layer
View Full Case Study โ†’
Beyond the Tutorial

What Makes Production MQTT Different from Demo Code

Reconnect logic looks simple until 200 ESP32 nodes recover simultaneously from a 10-minute WiFi outage. Without coordinated backoff, the broker receives a connection storm exceeding its maximum client limits, causing cascading failures. Production firmware adds jitter to backoff intervals, per-device reconnect windows, and broker-side connection rate limiting.

Topic access control matters at scale. A misconfigured device publishing to farm/# instead of its own zone can flood the broker's message queue and starve other subscribers. Broker ACL rules constrain each device to the topics it legitimately owns โ€” enforced server-side.

Data persistence is separate from MQTT delivery guarantees. QoS 2 gets the message to the broker exactly once โ€” it says nothing about what happens afterward. Time-series data requires a dedicated subscriber writing to InfluxDB, TimescaleDB, or AWS Timestream. Without it you show live data but lose all historical context.

Build Your IoT Dashboard with Us

From ESP32 firmware to React frontend, DigitalMonk handles the full stack โ€” production-grade MQTT architecture from day one.

Get a Free Project Estimate