ESP32 and Arduino

ESP32 WiFiManager MQTT Setup: Configure Wi-Fi and Broker Settings in a Captive Portal

Stop hard-coding credentials: let users enter Wi-Fi plus MQTT host, port, client ID, username and password in a WiFiManager captive portal, store them with Preferences and connect to broker.connectmq.com by default.

Phone showing a captive portal with broker fields for an ESP32 WiFiManager MQTT configuration
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

With ESP32 WiFiManager MQTT parameters, a device you hand to someone else can be configured from a phone: it opens a captive portal, the user picks a Wi-Fi network and fills in the broker settings, and the device remembers everything across reboots. This tutorial builds that flow with the WiFiManager library and Preferences, defaulting to broker.connectmq.com so it works out of the box.

The Problem With Hard-Coded Settings

Most MQTT examples start with lines like const char* WIFI_SSID = "..." and const char* MQTT_PASS = "...". That is fine on your desk, but every new network or credential change means recompiling and reflashing. It also means the firmware binary contains secrets, and you cannot give the same binary to two customers.

A provisioning portal solves this. On first boot, or when the user holds a button, the ESP32 starts its own access point. Connecting to it with a phone opens a web page where the user enters:

  • The Wi-Fi network and password.
  • The MQTT broker host and port.
  • The MQTT client ID, username and password.
  • A topic prefix for this device.

The device saves the values to non-volatile storage, joins the Wi-Fi network and connects to the broker. The same binary works for everyone.

Libraries and Hardware

  • WiFiManager by tzapu (2.x), installable from the Arduino Library Manager or PlatformIO as tzapu/WiFiManager.
  • PubSubClient for MQTT.
  • Preferences, which ships with the Arduino-ESP32 core and stores key–value pairs in NVS flash.
  • Any ESP32 board. The example uses the BOOT button (GPIO 0) on most DevKits to reopen the portal.

How the ESP32 WiFiManager MQTT Flow Works

Situation What happens
First boot, no saved Wi-Fi Portal starts as access point Sensor-Setup-xxxx
Saved Wi-Fi reachable Device connects silently, loads MQTT settings from Preferences
Saved Wi-Fi unreachable Portal opens again after the connection attempt fails
BOOT button held for 3 s at start-up Portal opens on demand so the user can change settings
Portal left unused for 3 minutes Device restarts and tries again, so it never hangs forever

WiFiManager stores the Wi-Fi credentials itself. The MQTT fields are custom parameters that you add to the portal and persist yourself.

Complete Sketch

#include <WiFi.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <PubSubClient.h>

const int CONFIG_BUTTON = 0;    // BOOT button on most ESP32 DevKits

Preferences prefs;
WiFiClient net;
PubSubClient mqtt(net);

char mqttHost[64]   = "broker.connectmq.com";
char mqttPort[6]    = "1883";
char mqttClient[48] = "";
char mqttUser[48]   = "";
char mqttPass[64]   = "";
char topicPrefix[64] = "connectmq/demo/d93f2a";

bool shouldSave = false;
unsigned long lastPublish = 0;

void loadSettings() {
  prefs.begin("mqtt", true);
  prefs.getString("host", mqttHost, sizeof(mqttHost));
  prefs.getString("port", mqttPort, sizeof(mqttPort));
  prefs.getString("client", mqttClient, sizeof(mqttClient));
  prefs.getString("user", mqttUser, sizeof(mqttUser));
  prefs.getString("pass", mqttPass, sizeof(mqttPass));
  prefs.getString("prefix", topicPrefix, sizeof(topicPrefix));
  prefs.end();

  if (strlen(mqttClient) == 0) {   // never connect with an empty client ID
    snprintf(mqttClient, sizeof(mqttClient), "esp32-%08x", (uint32_t)ESP.getEfuseMac());
  }
}

void saveSettings() {
  prefs.begin("mqtt", false);
  prefs.putString("host", mqttHost);
  prefs.putString("port", mqttPort);
  prefs.putString("client", mqttClient);
  prefs.putString("user", mqttUser);
  prefs.putString("pass", mqttPass);
  prefs.putString("prefix", topicPrefix);
  prefs.end();
}

void runWifiManager(bool forcePortal) {
  WiFiManager wm;

  WiFiManagerParameter pHost("host", "MQTT host", mqttHost, sizeof(mqttHost) - 1);
  WiFiManagerParameter pPort("port", "MQTT port (1883 or 8883)", mqttPort, sizeof(mqttPort) - 1);
  WiFiManagerParameter pClient("client", "MQTT client ID", mqttClient, sizeof(mqttClient) - 1);
  WiFiManagerParameter pUser("user", "MQTT username (optional)", mqttUser, sizeof(mqttUser) - 1);
  WiFiManagerParameter pPass("pass", "MQTT password (optional)", mqttPass, sizeof(mqttPass) - 1,
                             "type='password'");
  WiFiManagerParameter pPrefix("prefix", "Topic prefix", topicPrefix, sizeof(topicPrefix) - 1);

  wm.addParameter(&pHost);
  wm.addParameter(&pPort);
  wm.addParameter(&pClient);
  wm.addParameter(&pUser);
  wm.addParameter(&pPass);
  wm.addParameter(&pPrefix);

  wm.setSaveParamsCallback([]() { shouldSave = true; });
  wm.setConfigPortalTimeout(180);

  char apName[32];
  snprintf(apName, sizeof(apName), "Sensor-Setup-%04x", (uint16_t)ESP.getEfuseMac());

  bool connected = forcePortal ? wm.startConfigPortal(apName)
                               : wm.autoConnect(apName);
  if (!connected) {
    ESP.restart();               // portal timed out: try again from scratch
  }

  if (shouldSave) {
    strlcpy(mqttHost, pHost.getValue(), sizeof(mqttHost));
    strlcpy(mqttPort, pPort.getValue(), sizeof(mqttPort));
    strlcpy(mqttClient, pClient.getValue(), sizeof(mqttClient));
    strlcpy(mqttUser, pUser.getValue(), sizeof(mqttUser));
    strlcpy(mqttPass, pPass.getValue(), sizeof(mqttPass));
    strlcpy(topicPrefix, pPrefix.getValue(), sizeof(topicPrefix));
    saveSettings();
    loadSettings();              // re-applies the client ID fallback
  }
}

bool buttonHeld(unsigned long ms) {
  pinMode(CONFIG_BUTTON, INPUT_PULLUP);
  unsigned long start = millis();
  while (digitalRead(CONFIG_BUTTON) == LOW) {
    if (millis() - start > ms) return true;
    delay(10);
  }
  return false;
}

void connectMqtt() {
  while (!mqtt.connected()) {
    bool ok = strlen(mqttUser) > 0
                ? mqtt.connect(mqttClient, mqttUser, mqttPass)
                : mqtt.connect(mqttClient);
    if (ok) {
      String cmd = String(topicPrefix) + "/cmd";
      mqtt.subscribe(cmd.c_str());
    } else {
      Serial.printf("MQTT failed state=%d, retrying\n", mqtt.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  loadSettings();
  delay(200);
  runWifiManager(buttonHeld(3000));

  mqtt.setServer(mqttHost, atoi(mqttPort));
  Serial.printf("Broker %s:%s as %s\n", mqttHost, mqttPort, mqttClient);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  if (millis() - lastPublish > 10000) {
    lastPublish = millis();
    String topic = String(topicPrefix) + "/telemetry";
    String payload = "{\"uptime\":" + String(millis() / 1000) + "}";
    mqtt.publish(topic.c_str(), payload.c_str());
  }
}

Walking Through the Portal

  1. Flash the sketch and open the serial monitor.
  2. On your phone, join the Wi-Fi network named Sensor-Setup-xxxx. Most phones open the captive portal automatically; otherwise browse to 192.168.4.1.
  3. Tap Configure WiFi, pick your network and enter its password.
  4. Leave the defaults to use the public area of broker.connectmq.com, or enter your own settings, then save.
  5. The device joins your network, connects to the broker and starts publishing to <prefix>/telemetry.

Verify from a computer:

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-d93f2a -t 'connectmq/demo/d93f2a/#' -v

Each device that keeps the default prefix publishes to the same topic, so if you set up several, give each one its own prefix in the portal.

Details That Make It Reliable

Load before you build the portal. The custom parameters are created with the current values as defaults, so the portal shows what is stored, not a blank form. Users editing one field do not lose the others.

Never connect with an empty client ID. If the user clears the field, loadSettings() falls back to an ID derived from the chip's MAC. An empty or duplicated client ID is one of the top reasons devices fail to connect or kick each other off; see why an ESP32 cannot connect to an MQTT broker for the full list.

Use the portal timeout. Without setConfigPortalTimeout(), a device whose router is temporarily down sits in portal mode forever. With a timeout it restarts and retries the saved network.

Store numbers as strings in the portal, convert once. WiFiManager parameters are text fields. Keep the port as a string in storage and convert with atoi() when configuring the client.

Hide the password field. The extra argument type='password' masks the MQTT password in the portal. The value is still sent over the open access point in plain HTTP, so only provision in a trusted location.

Validating What Users Type

A captive portal accepts whatever the user enters, so the firmware has to cope with mistakes. A few checks after saving prevent a device from bricking itself with bad settings:

  • Port: accept only values between 1 and 65535. If atoi() returns 0 because the field contained text, fall back to 1883.
  • Host: trim spaces and reject an empty string. Users often paste a URL such as mqtt://broker.connectmq.com; strip the scheme and anything after the host name.
  • Topic prefix: remove a trailing slash and reject wildcards (+ and #), which are not allowed in publish topics.
  • Credentials: if a username is set but the password is empty, warn on the serial console, because the broker will refuse the connection.

When the MQTT connection fails repeatedly with a positive mqtt.state() code (bad credentials or not authorised), it is usually a settings problem rather than a network one. A good pattern is to count those failures and reopen the portal automatically after several in a row, so the user can correct the values without needing a button or a serial cable.

Adding TLS for Production Brokers

Port 1883 is plain text and fine for the public test area. For a private broker, switch to WiFiClientSecure when the user chooses port 8883:

#include <WiFiClientSecure.h>

WiFiClientSecure secureNet;

void configureTransport() {
  if (atoi(mqttPort) == 8883) {
    secureNet.setCACert(ISRG_ROOT_X1);   // Let's Encrypt root, stored as a PEM string
    mqtt.setClient(secureNet);
  } else {
    mqtt.setClient(net);
  }
  mqtt.setServer(mqttHost, atoi(mqttPort));
}

Call configureTransport() instead of the plain setServer() line in setup(). With ConnectMQ, users then enter the client ID, username and password generated for their broker in the portal (YOUR_CLIENT_ID, YOUR_USERNAME, YOUR_PASSWORD). The client ID is bound to those credentials, so it has to be entered exactly as issued. The ESP32 MQTT SSL/TLS guide explains how to embed the root certificate.

Troubleshooting

Problem Likely cause Fix
Portal never appears Saved Wi-Fi still connects Hold the BOOT button during reset, or call wm.resetSettings() once
Parameters do not save Save callback not triggered Use setSaveParamsCallback() and press Save on the portal page
Values truncated Field length smaller than input Increase the buffer size and the parameter length
Connects to Wi-Fi but not MQTT Wrong port for the transport, or wrong credentials Check mqtt.state() and the port/TLS combination
Phone does not open the portal Captive detection blocked Browse manually to 192.168.4.1

Where to Go Next

Once settings are configurable, the natural next steps are remote updates and status reporting: ESP32 OTA updates triggered over MQTT shows how to ship new firmware without USB, and the ESP32 IoT tutorial puts provisioning into a complete device project.

Provision Devices for ConnectMQ

The default settings point at the free public MQTT broker, so a fresh device works the moment it joins Wi-Fi. For deployments, create a private broker on ConnectMQ and enter its credentials in the portal. ConnectMQ is a managed MQTT broker, so the people configuring devices never need to run a server. Start on the free Serverless plan at app.connectmq.com, no credit card required.

Frequently Asked Questions

Where does WiFiManager store the Wi-Fi password?

The ESP32 Wi-Fi driver stores the network credentials in NVS flash. The MQTT fields are stored separately in this sketch using Preferences under the mqtt namespace.

How do I reopen the WiFiManager portal later?

Hold the BOOT button for three seconds during start-up in this sketch, which calls startConfigPortal(). You could also trigger it from an MQTT command, as long as the device is still reachable.

Can I use WiFiManager with AsyncMqttClient?

Yes. WiFiManager only handles provisioning. Once it returns, use the stored values to configure whichever MQTT client you prefer.

Is the captive portal secure?

The portal runs as an open access point over plain HTTP, so anyone nearby could observe the values during setup. Provision in a trusted place, and consider setting a password on the setup access point.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 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.