An ESP32 DHT22 MQTT sensor is the classic first IoT project, and it is worth building properly. In this tutorial you will wire a DHT22, read it without the usual NaN errors, publish temperature, humidity and heat index as JSON to broker.connectmq.com, and then watch the readings live in the ConnectMQ topic explorer.
What You Need
- An ESP32 development board (any classic ESP32, S3, C3 or C6).
- A DHT22 (also sold as AM2302), either the bare 4-pin sensor or a 3-pin breakout module.
- A 10 kOhm resistor if you use the bare sensor. Breakout modules usually include it.
- Jumper wires and a breadboard.
- Arduino IDE 2.x with the esp32 board package.
- Libraries from the Library Manager: DHT sensor library by Adafruit, Adafruit Unified Sensor, and PubSubClient by Nick O'Leary.
Wiring the DHT22 to the ESP32
| DHT22 pin | Bare sensor pin number | ESP32 pin | Notes |
|---|---|---|---|
| VCC | 1 | 3V3 | 3.3 V works; keep wires short |
| DATA | 2 | GPIO 4 | 10 kOhm pull-up from DATA to 3V3 |
| NC | 3 | not connected | Bare sensor only |
| GND | 4 | GND |
GPIO 4 is a safe choice on a classic ESP32 because it has no boot-strapping role. Avoid GPIO 0, 2, 12 and 15 for sensors; their state at reset affects boot. On the C3 and C6, pick any free GPIO that is not a strapping pin and change the DHT_PIN constant.
Keep the sensor a few centimetres away from the ESP32. The board's regulator and radio warm up during operation, and a DHT22 taped directly to the board will read a degree or two high.
How the DHT22 Behaves
The DHT22 uses a single-wire protocol with strict timing. Two properties shape the code:
- It can only be read every 2 seconds. Reading faster returns cached values or errors. For MQTT telemetry, once every 10 to 60 seconds is plenty.
- Reads occasionally fail. A Wi-Fi interrupt at the wrong moment corrupts the bit timing, and the library returns
NaN. Production code must check for this and retry rather than publish garbage.
Accuracy is roughly half a degree Celsius and a few percent relative humidity. If you need better humidity accuracy or a pressure reading, the ESP32 BME280 tutorial uses an I2C sensor with less timing sensitivity.
Topic and Payload Design
Use a unique prefix so your data does not collide with other users of the public test broker:
| Topic | Retained | Payload |
|---|---|---|
connectmq/demo/dht7w3p/telemetry |
No | {"temperature":22.4,"humidity":48.1,"heatIndex":22.1,"rssi":-61} |
connectmq/demo/dht7w3p/status |
Yes | online or offline (last will) |
One JSON object per reading keeps related values together, so a consumer never sees a temperature paired with a humidity value from a different moment.
The Complete ESP32 DHT22 MQTT Sketch
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
// ---------- settings ----------
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/dht7w3p"; // change to your own random id
const uint8_t DHT_PIN = 4;
const uint32_t INTERVAL_MS = 15000;
DHT dht(DHT_PIN, DHT22);
WiFiClient net;
PubSubClient mqtt(net);
String clientId, topicTelemetry, topicStatus;
unsigned long lastRead = 0;
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(400);
Serial.print(".");
}
Serial.printf(" connected, IP %s\n", WiFi.localIP().toString().c_str());
}
void connectMqtt() {
while (!mqtt.connected()) {
Serial.print("MQTT... ");
if (mqtt.connect(clientId.c_str(), topicStatus.c_str(), 1, true, "offline")) {
Serial.println("connected");
mqtt.publish(topicStatus.c_str(), "online", true);
} else {
Serial.printf("failed (state %d), retry in 5 s\n", mqtt.state());
delay(5000);
}
}
}
bool readDht(float& t, float& h) {
for (int attempt = 0; attempt < 3; attempt++) {
h = dht.readHumidity();
t = dht.readTemperature(); // Celsius
if (!isnan(h) && !isnan(t)) return true;
delay(2100); // DHT22 needs 2 s between reads
}
return false;
}
void setup() {
Serial.begin(115200);
dht.begin();
clientId = "esp32-dht22-" + String((uint32_t)ESP.getEfuseMac(), HEX);
topicTelemetry = String(PREFIX) + "/telemetry";
topicStatus = String(PREFIX) + "/status";
connectWifi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setBufferSize(512);
connectMqtt();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (millis() - lastRead < INTERVAL_MS) return;
lastRead = millis();
float t, h;
if (!readDht(t, h)) {
Serial.println("DHT22 read failed three times, skipping this cycle");
mqtt.publish(topicStatus.c_str(), "sensor-error", true);
return;
}
float heatIndex = dht.computeHeatIndex(t, h, false); // false = Celsius
char payload[160];
snprintf(payload, sizeof(payload),
"{\"temperature\":%.1f,\"humidity\":%.1f,\"heatIndex\":%.1f,\"rssi\":%d}",
t, h, heatIndex, WiFi.RSSI());
if (mqtt.publish(topicTelemetry.c_str(), payload)) {
Serial.println(payload);
} else {
Serial.println("publish failed");
}
}
A note on the status topic: after a sensor error the sketch publishes sensor-error as a retained status, and the next successful connect overwrites it with online. If you prefer a separate health topic, add one; the point is that a broken sensor is visible instead of silent.
Choosing an Interval and Saving Power
The 15-second interval in the sketch suits a desk demo. For real rooms, think about what the data is for. Comfort monitoring rarely needs more than one reading per minute, and a greenhouse or server cupboard alarm might want every 30 seconds. Publishing less often reduces broker traffic and makes the data easier to chart.
If the node runs on batteries, keeping Wi-Fi connected all the time is the biggest drain. A better structure is: wake from deep sleep, power the sensor, wait two seconds, read, connect to Wi-Fi and the broker, publish, disconnect cleanly, then call esp_deep_sleep_start() with a timer wakeup. In that mode the last will is less useful, because every sleep looks like a disconnect, so publish a retained lastSeen value instead and let the dashboard flag sensors that have not reported for a few intervals. Power the DHT22 from a GPIO pin if you want it fully off during sleep, and remember to wait the full two seconds after powering it before the first read.
Testing With the Command Line
Subscribe with the Mosquitto client, using any unique client ID:
mosquitto_sub -h broker.connectmq.com -p 1883 -i dht-viewer-7w3p -t "connectmq/demo/dht7w3p/#" -v
Breathe on the sensor and the humidity jumps within one or two readings. Remember that the public area is shared and readable by anyone, so it is fine for a desk demo but not for data you want to keep private. The free public MQTT broker guide lists its limits.
Viewing ESP32 DHT22 MQTT Data in the Topic Explorer
The command line is quick, but a visual view is nicer when you run a few sensors. Create a free account at app.connectmq.com, choose the free Serverless plan, create a broker and copy the generated username, password and client ID. Then change the sketch to connect with credentials over TLS:
#include <WiFiClientSecure.h>
WiFiClientSecure net; // replaces WiFiClient
PubSubClient mqtt(net);
// ISRG Root X1 (Let's Encrypt) in PEM format, copied from letsencrypt.org
static const char ROOT_CA[] = R"PEM(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 here...
-----END CERTIFICATE-----
)PEM";
// in setup(), before connectMqtt():
net.setCACert(ROOT_CA);
mqtt.setServer("broker.connectmq.com", 8883);
// in connectMqtt():
mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
topicStatus.c_str(), 1, true, "offline");
Open your broker in the ConnectMQ portal and go to the live topic explorer. Your telemetry topic appears in the tree, and each new reading updates in place. From the same portal you can add a threshold alert rule, for example humidity above 70 percent, and review triggered alerts in the alerts log. For long-term charts, run Node-RED, Telegraf or a small Python subscriber that connects to the broker as an MQTT client and writes to the database of your choice.
Troubleshooting
- Every read returns NaN. Check the pull-up resistor and that DATA really goes to the pin set in
DHT_PIN. A missing pull-up is the most common cause on bare sensors. - Occasional NaN. Normal under Wi-Fi load. The retry loop handles it; do not reduce the 2-second gap.
- Temperature stuck or slow to change. The DHT22 responds slowly, taking tens of seconds to settle. That is the sensor, not MQTT.
- Readings a couple of degrees high. Self-heating from the board. Move the sensor away on a short cable.
- Long cables give errors. The single-wire protocol is sensitive. Keep the cable short or lower the pull-up value slightly.
- MQTT state -2. TCP connect failed; check Wi-Fi and that outbound 1883 is allowed.
To format larger payloads safely, see ArduinoJson for ESP32 MQTT payloads. If you need several temperature points but no humidity, the DS18B20 multi-sensor project is a better fit.
Get Started with ConnectMQ
A DHT22 node becomes useful when its data lands somewhere reliable. ConnectMQ gives you a cloud MQTT broker with TLS on 8883, MQTT 3.1.1 and 5.0, a live topic explorer and alert rules, and a free Serverless plan with no credit card. Flash the TLS version of this sketch with your credentials, and continue with the ESP32 IoT tutorial for more sensor and device projects.
Frequently Asked Questions
How often should an ESP32 publish DHT22 readings over MQTT?
Every 10 to 60 seconds is typical for room monitoring. The DHT22 cannot be read faster than every 2 seconds, and its slow response makes faster publishing pointless.
Can I use a DHT11 instead of a DHT22?
Yes. Change DHT22 to DHT11 in the constructor. The DHT11 has lower accuracy and range, so the DHT22 or a BME280 is better for anything beyond a demo.
Should I publish temperature and humidity on separate topics?
Either works. A single JSON object keeps the values from one moment together, while separate topics are handy for tools that expect one number per topic.
Why use a retained status topic with a last will?
The broker publishes the retained offline message if the ESP32 disappears without disconnecting, so any dashboard can tell a dead sensor from a quiet one.



