This ESP32 IoT tutorial takes you from an unboxed board to a cloud-connected sensor that publishes temperature and humidity, accepts remote commands and talks to the cloud over TLS. You will set up the Arduino IDE, connect to Wi-Fi, read a DHT22, publish over MQTT to a free public broker, and then move the same sketch to a private, encrypted broker for real use.
Why the ESP32 Is the Default IoT Microcontroller
The ESP32 family from Espressif combines a fast dual-core (or single-core RISC-V, depending on the variant) processor, built-in Wi-Fi and Bluetooth, plenty of GPIO, analog inputs and hardware support for TLS in a module that costs a few dollars. It runs Arduino code, MicroPython and Espressif's own ESP-IDF framework, and it has an enormous community. For anyone building a connected sensor, a relay controller or a small gateway, it is usually the first board to try.
What you will build in this tutorial:
- A board that joins your Wi-Fi network and reconnects automatically.
- A DHT22 sensor publishing JSON readings every ten seconds over MQTT.
- A remote-controlled LED, switched by publishing
ONorOFFto a command topic. - An online/offline status topic using MQTT's Last Will feature.
- A secure version using TLS on port 8883 with private credentials.
What You Need
| Item | Notes |
|---|---|
| ESP32 development board | Any ESP32 DevKit, ESP32-S3 or ESP32-C3 board with USB works |
| DHT22 (AM2302) sensor | A breakout module with a built-in pull-up resistor is easiest |
| Jumper wires and breadboard | Three wires for the sensor |
| USB cable | Must be a data cable, not charge-only |
| Computer with Arduino IDE 2.x | Windows, macOS or Linux |
| Mosquitto command-line clients | mosquitto_pub and mosquitto_sub for testing |
No sensor yet? The sketch reports an error and keeps running, so you can still follow the MQTT and LED parts.
Step 1: Set Up the Arduino IDE for ESP32
- Install Arduino IDE 2.x from the Arduino website.
- Open Tools, Board, Boards Manager, search for esp32 and install esp32 by Espressif Systems. If it does not appear, add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonunder File, Preferences, Additional boards manager URLs and search again. The official Arduino-ESP32 installation guide has screenshots for each platform. - Open Tools, Manage Libraries and install PubSubClient by Nick O'Leary and DHT sensor library by Adafruit (accept the prompt to install Adafruit Unified Sensor as well).
- Plug in the board, select your model under Tools, Board (for a generic DevKit, choose ESP32 Dev Module) and select the serial port under Tools, Port.
Test the toolchain with a blink sketch before going further:
#define LED_PIN 2 // onboard LED on most ESP32 DevKit boards
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
}
If the upload stalls at "Connecting...", hold the BOOT button on the board until the upload starts. If no port appears at all, install the USB-to-serial driver for your board's chip (often CP210x or CH340) or try another cable. Some boards have no LED on GPIO 2; in that case, wire an LED with a 220 ohm resistor to GPIO 2.
Step 2: Wire the DHT22 Sensor
Connect the DHT22 module as follows:
| DHT22 pin | ESP32 pin |
|---|---|
| VCC (+) | 3V3 |
| DATA (out) | GPIO 4 |
| GND (-) | GND |
If you use a bare four-pin sensor instead of a module, add a 10 kilohm pull-up resistor between DATA and 3V3, and leave the third pin unconnected. Avoid GPIO 6 to 11, which are reserved for flash memory on classic ESP32 modules, and avoid strapping pins such as GPIO 0, 2, 12 and 15 for sensors. The DHT22 should not be read more often than every two seconds.
Step 3: Understand the MQTT Pieces
MQTT is a publish/subscribe protocol. Your ESP32 does not send data to a specific server application; it publishes messages to topics on a broker, and any client subscribed to those topics receives them. If you want the full background, read what an MQTT broker does first.
For this ESP32 IoT tutorial we use the free public MQTT broker at broker.connectmq.com. It accepts anonymous connections on port 1883 as long as each client uses a unique, non-empty client ID. Everything published in the public area is readable by anyone, so never send passwords or personal data, and use a unique topic prefix so your messages do not collide with others. The public area allows 64 KB per message, 10 messages per second and 10 subscriptions per client, which is far more than a sensor needs.
Our topics, with k3v9 replaced by your own random string:
connectmq/demo/esp32-k3v9/telemetry JSON readings, published every 10 s
connectmq/demo/esp32-k3v9/status "online" / "offline" (retained, Last Will)
connectmq/demo/esp32-k3v9/led/set commands: ON or OFF
connectmq/demo/esp32-k3v9/led/state current LED state (retained)
Step 4: The Complete ESP32 IoT Sketch
Create a new sketch, paste the code below, and set your Wi-Fi name, password and topic prefix.
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
// ---------- configuration ----------
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* TOPIC_PREFIX = "connectmq/demo/esp32-k3v9"; // change k3v9 to your own string
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define LED_PIN 2
const unsigned long PUBLISH_INTERVAL_MS = 10000;
// -----------------------------------
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient net;
PubSubClient mqtt(net);
String clientId;
String topicTelemetry, topicStatus, topicLedSet, topicLedState;
unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;
void connectWiFi() {
if (WiFi.status() == WL_CONNECTED) return;
Serial.printf("Connecting to Wi-Fi %s", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nWi-Fi connected, IP %s\n", WiFi.localIP().toString().c_str());
} else {
Serial.println("\nWi-Fi connection failed, will retry");
}
}
void setLed(bool on) {
digitalWrite(LED_PIN, on ? HIGH : LOW);
mqtt.publish(topicLedState.c_str(), on ? "ON" : "OFF", true); // retained state
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String message;
for (unsigned int i = 0; i < length; i++) message += (char)payload[i];
message.trim();
Serial.printf("Received [%s] %s\n", topic, message.c_str());
if (topicLedSet == topic) {
if (message == "ON") setLed(true);
else if (message == "OFF") setLed(false);
}
}
bool connectMqtt() {
Serial.printf("Connecting to MQTT %s:%u as %s\n", MQTT_HOST, MQTT_PORT, clientId.c_str());
// Last Will: the broker publishes "offline" (retained) if we disappear
bool ok = mqtt.connect(clientId.c_str(), topicStatus.c_str(), 1, true, "offline");
if (ok) {
Serial.println("MQTT connected");
mqtt.publish(topicStatus.c_str(), "online", true);
mqtt.subscribe(topicLedSet.c_str(), 1);
} else {
Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
}
return ok;
}
void publishReading() {
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
if (isnan(temperature) || isnan(humidity)) {
Serial.println("DHT22 read failed, check wiring");
mqtt.publish(topicTelemetry.c_str(), "{\"error\":\"sensor_read_failed\"}");
return;
}
char payload[128];
snprintf(payload, sizeof(payload),
"{\"temperature\":%.2f,\"humidity\":%.1f,\"rssi\":%d,\"uptime\":%lu}",
temperature, humidity, WiFi.RSSI(), millis() / 1000);
mqtt.publish(topicTelemetry.c_str(), payload);
Serial.printf("Published %s\n", payload);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
dht.begin();
// Unique client ID derived from the chip's MAC address
uint64_t mac = ESP.getEfuseMac();
char id[32];
snprintf(id, sizeof(id), "esp32-%04X%08X", (uint16_t)(mac >> 32), (uint32_t)mac);
clientId = id;
topicTelemetry = String(TOPIC_PREFIX) + "/telemetry";
topicStatus = String(TOPIC_PREFIX) + "/status";
topicLedSet = String(TOPIC_PREFIX) + "/led/set";
topicLedState = String(TOPIC_PREFIX) + "/led/state";
connectWiFi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
return;
}
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnectAttempt > 5000) { // non-blocking retry every 5 s
lastReconnectAttempt = now;
if (connectMqtt()) lastReconnectAttempt = 0;
}
return;
}
mqtt.loop();
if (millis() - lastPublish >= PUBLISH_INTERVAL_MS) {
lastPublish = millis();
publishReading();
}
}
Upload the sketch and open the Serial Monitor at 115200 baud. You should see the Wi-Fi connection, the MQTT connection and a published reading every ten seconds.
How the sketch is structured
- Non-blocking reconnects. The loop never waits in a tight retry loop. If Wi-Fi or MQTT drops, it retries every few seconds while the rest of the code stays responsive. Long blocking loops are a common source of watchdog resets; the ESP32 MQTT reconnect guide explains more robust strategies.
- Last Will and retained status. The broker stores
onlineas the retained status and automatically replaces it withofflineif the board loses power or connectivity without a clean disconnect. - Retained LED state. Any dashboard that subscribes later immediately learns whether the LED is on.
- Small payloads. PubSubClient's default packet buffer is 256 bytes, which this payload fits comfortably. For larger JSON documents, call
mqtt.setBufferSize(1024)before connecting, as covered in PubSubClient buffer size.
Step 5: Test from Your Computer
Watch everything your board publishes:
mosquitto_sub -h broker.connectmq.com -p 1883 -i laptop-k3v9-sub -t "connectmq/demo/esp32-k3v9/#" -v
You should see the retained status and led/state messages immediately, then telemetry every ten seconds:
connectmq/demo/esp32-k3v9/status online
connectmq/demo/esp32-k3v9/telemetry {"temperature":23.40,"humidity":48.2,"rssi":-58,"uptime":42}
Now switch the LED from a second terminal:
mosquitto_pub -h broker.connectmq.com -p 1883 -i laptop-k3v9-pub -t "connectmq/demo/esp32-k3v9/led/set" -m ON
mosquitto_pub -h broker.connectmq.com -p 1883 -i laptop-k3v9-pub -t "connectmq/demo/esp32-k3v9/led/set" -m OFF
The LED switches and a new led/state message appears in the subscriber. Unplug the board, wait about 45 seconds (1.5 times the keepalive), and the broker publishes offline on the status topic. That is your complete IoT loop: sensing, publishing, remote control and presence detection.
Do not publish commands with the retain flag (-r). A retained command would be delivered again every time the ESP32 reconnects.
Step 6: Move to a Private Broker with TLS
The public broker is perfect for learning, but a real device needs privacy, authentication and encryption. With ConnectMQ you get a private broker with generated credentials and a Let's Encrypt certificate on port 8883.
- Sign up at the ConnectMQ portal, choose the free Serverless plan, create a broker and copy the generated username, password and client ID.
- Download the Let's Encrypt root certificate (ISRG Root X1) that the broker's certificate chains to:
curl -o isrgrootx1.pem https://letsencrypt.org/certs/isrgrootx1.pem
- Change the top of the sketch as follows. Only the includes, configuration, client object and connect call change; everything else stays the same.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <DHT.h>
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 8883;
const char* MQTT_USERNAME = "YOUR_USERNAME";
const char* MQTT_PASSWORD = "YOUR_PASSWORD";
const char* MQTT_CLIENT_ID = "YOUR_CLIENT_ID";
const char* TOPIC_PREFIX = "home/office/esp32-01";
// Paste the full contents of isrgrootx1.pem between the markers
const char* ROOT_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the certificate body from isrgrootx1.pem here...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure net;
PubSubClient mqtt(net);
In setup(), replace the MAC-based client ID with the one from the portal and load the certificate before connecting:
clientId = MQTT_CLIENT_ID; // must match the client ID issued with your credentials
net.setCACert(ROOT_CA);
And in connectMqtt(), pass the credentials along with the Last Will:
bool ok = mqtt.connect(clientId.c_str(), MQTT_USERNAME, MQTT_PASSWORD,
topicStatus.c_str(), 1, true, "offline");
Upload again. The Serial Monitor should show a successful connection on port 8883. Verify from your computer with the same credentials, or open the in-browser MQTT client and live topic explorer in the portal to watch the readings arrive. Keep in mind that ConnectMQ binds client IDs to the credentials it issues, so each device needs its own credential set; reusing one set on two boards makes them disconnect each other. The ESP32 MQTT over SSL tutorial covers certificate handling in more depth, including why setInsecure() should never ship in production.
Step 7: Make the Sensor Ready for the Real World
A sketch that runs on your desk for an afternoon is different from a device that runs unattended for a year. Before you install the board somewhere hard to reach, work through this checklist:
- Stable power. Use a quality 5 V supply rated for at least 500 mA. Wi-Fi transmission causes current spikes, and cheap adapters cause brownout resets that look like software bugs.
- Sensor placement. Keep the DHT22 away from the ESP32 itself and from voltage regulators, which warm up and bias readings by a degree or more. A short cable and a ventilated enclosure help.
- One credential set per device. Never copy the same username, password and client ID onto several boards. Issue a set per device so you can revoke one without touching the others.
- Real timestamps. The
uptimefield is useful for debugging, but analysis needs wall-clock time. Sync the clock with NTP at boot and add an epoch timestamp to each reading, as shown in ESP32 NTP timestamps for MQTT. - Remote updates. Climbing a ladder to reflash a sensor gets old quickly. Plan for over-the-air updates from the start with ESP32 OTA updates over MQTT.
- Configuration without recompiling. Hard-coded Wi-Fi credentials are fine for one board. For several, use a captive portal library so each device can be configured on site.
- Health reporting. Keep publishing RSSI and uptime. A slowly falling signal strength or frequent restarts are early warnings that a device will soon go offline.
None of these steps changes the MQTT logic you already wrote. They make the difference between a demo and a sensor you can forget about because it simply keeps reporting.
Troubleshooting
If mqtt.state() returns a non-zero value, this table usually points to the cause:
| state() | Meaning | Typical fix |
|---|---|---|
| -4 | Connection timeout | Check network, firewall, port number and keepalive |
| -2 | TCP or TLS connect failed | Wrong host or port, missing CA certificate, Wi-Fi not ready |
| -3 / -1 | Connection lost / disconnected | Duplicate client ID, unstable Wi-Fi, blocking code in loop |
| 2 | Client ID rejected | Empty or wrong client ID; use the one issued with your credentials |
| 4 | Bad username or password | Re-copy credentials from the portal, check for spaces |
| 5 | Not authorized | Credentials do not match the client ID or broker |
Other common issues:
- Readings are
nan. Check the DHT22 wiring, the pull-up resistor and that you read no faster than every two seconds. - Publish silently fails. The message is larger than the PubSubClient buffer; increase it with
setBufferSize(). - Board resets randomly. Weak USB power during Wi-Fi transmission; try a different port or a powered hub.
- Works on 1883 but not 8883. The CA certificate is missing or incomplete, or the network blocks outbound port 8883.
For a longer checklist, see ESP32 cannot connect to the MQTT broker.
Where to Go Next
With this foundation you can build almost any connected sensor or actuator. Good next steps:
- Replace hand-built strings with ArduinoJson, as shown in ESP32 ArduinoJson and MQTT.
- Swap the DHT22 for a more accurate BME280 over MQTT.
- Run on batteries with ESP32 deep sleep and MQTT.
- Switch real loads safely with an ESP32 relay controlled over MQTT.
- Compare this approach with the shorter, LED-focused ESP32 MQTT tutorial and the Python route in MicroPython umqtt on the ESP32.
Get Your ESP32 Online with ConnectMQ
When your prototype is ready for real data, give it a private home. Create a free account at app.connectmq.com on the Serverless plan, no credit card required, create a broker and flash the generated credentials into your sketch. The free plan supports up to 25 connections, which covers a small fleet of sensors plus a dashboard or two, and when you outgrow it a managed MQTT broker plan scales to hundreds of devices without changing your code.
Frequently Asked Questions
Which ESP32 board is best for beginners?
A standard ESP32 DevKit (often labeled ESP32-WROOM-32 or ESP32 Dev Module) is the easiest start because almost every tutorial and library supports it. ESP32-S3 and ESP32-C3 boards also work with the code in this tutorial.
Do I need an account to follow this ESP32 IoT tutorial?
No. The first part uses the public area of broker.connectmq.com, which accepts anonymous connections with any unique non-empty client ID. You only need an account when you move to a private broker with TLS.
Why does my ESP32 keep disconnecting from the broker?
The most common causes are two devices using the same client ID, blocking code that prevents mqtt.loop() from running, and weak Wi-Fi signal. Check the RSSI value in the telemetry and make sure every board has its own client ID.
Does PubSubClient support MQTT 5?
No. PubSubClient implements MQTT 3.1.1, which is fully supported by the broker. If you need MQTT 5 features on the ESP32, use the ESP-IDF esp-mqtt component instead.
Is it safe to use setInsecure() for TLS on the ESP32?
Only for quick experiments. setInsecure() encrypts traffic but skips server verification, so it does not protect against impersonation. Load the root CA certificate with setCACert() for anything beyond a test.



