You have ten sensor nodes spread across a warehouse floor. The WiFi router sits in one corner, and half your ESP32 boards cannot even see it. You could add range extenders, run Ethernet cables, or install multiple access points — all of which cost money and add complexity. Or you could let the ESP32 boards talk to each other and relay messages until the data reaches one node that does have internet access.
That is mesh networking, and the ESP32 is exceptionally good at it.
This guide walks through everything you need to build real mesh networks with ESP32: the painlessMesh library for full mesh topologies, ESP-NOW for simpler point-to-multipoint links, working code for both, and practical advice for deploying these networks in Indian conditions — large homes, farms, factories, and exhibitions.
The Range Problem
A single WiFi access point covers roughly 30-50 metres indoors and 100 metres outdoors under ideal conditions. In practice, concrete walls, metal shelving, and electrical interference cut that range dramatically. Indian construction with thick brick walls and RCC slabs is particularly punishing for 2.4 GHz signals.
When you need sensors in every room of a large building, across an agricultural field, or along a factory production line, a single router simply cannot reach every node. More importantly, most of your nodes do not need internet access at all — they just need to get their data to one central collection point.
This is where mesh networking changes the game.
What Is Mesh Networking?
In a mesh network, every node can relay messages for other nodes. Instead of every device talking directly to a central router, devices talk to their nearest neighbours, who forward the message along until it reaches its destination.
Key properties of a mesh network:
- Self-healing: If one node goes down, traffic automatically routes around it through other nodes
- No single point of failure: The network adapts to node additions and removals dynamically
- Extended range: Each node only needs to reach its nearest neighbour, not the central router
- Scalability: Adding nodes can actually improve coverage rather than degrade it
Think of it like a bucket brigade — no single person needs to run the entire distance; they just pass the bucket to the next person in line.
ESP-MESH: How It Works
Espressif's ESP-MESH protocol builds a mesh network using the ESP32's WiFi radio. It creates a tree topology where:
- A root node connects to the actual WiFi router (and therefore the internet)
- Layer 1 nodes connect to the root node
- Layer 2 nodes connect to Layer 1 nodes, and so on
- Messages hop from node to node until they reach their destination
The tree can go up to 25 layers deep in theory, though practical deployments work best with 4-6 layers. Each node acts as both a station (connecting to its parent) and an access point (allowing children to connect).
The ESP-MESH protocol handles topology management automatically — nodes discover each other, negotiate parent-child relationships, and reorganise themselves when nodes join or leave the network.
painlessMesh: The Easy Way to Build a Mesh
While you can use Espressif's ESP-MESH API directly, the painlessMesh library makes it dramatically simpler. It wraps the complexity of mesh management into a clean Arduino-compatible API.
Install it in the Arduino IDE or PlatformIO:
# PlatformIO (platformio.ini)
lib_deps = painlessMesh/painlessMesh@^1.5.0
# Arduino IDE
# Search "painlessMesh" in Library Manager and install
painlessMesh requires these dependencies (installed automatically with PlatformIO):
- ArduinoJson (v6.x)
- TaskScheduler
- AsyncTCP (for ESP32)
Basic Mesh: Three Nodes Talking to Each Other
This is the simplest possible mesh. Every node broadcasts a message, and every other node receives it.
#include <painlessMesh.h>
#define MESH_PREFIX "wavtronMesh"
#define MESH_PASSWORD "meshPassword123"
#define MESH_PORT 5555
painlessMesh mesh;
Scheduler userScheduler;
// Send a message every 5 seconds
Task taskSendMessage(TASK_SECOND * 5, TASK_FOREVER, []() {
String msg = "Hello from node ";
msg += mesh.getNodeId();
msg += " | uptime: ";
msg += millis() / 1000;
msg += "s";
mesh.sendBroadcast(msg);
Serial.printf("Broadcast: %s\n", msg.c_str());
});
void receivedCallback(uint32_t from, String &msg) {
Serial.printf("Received from %u: %s\n", from, msg.c_str());
}
void newConnectionCallback(uint32_t nodeId) {
Serial.printf("New connection: %u\n", nodeId);
}
void changedConnectionCallback() {
Serial.printf("Topology changed. Nodes: %d\n",
mesh.getNodeList().size() + 1);
}
void setup() {
Serial.begin(115200);
mesh.setDebugMsgTypes(ERROR | STARTUP);
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
userScheduler.addTask(taskSendMessage);
taskSendMessage.enable();
}
void loop() {
mesh.update();
}
Flash this same sketch to three ESP32 boards. Within seconds, they discover each other, form a mesh, and start exchanging messages. Open Serial Monitor on any board to watch the traffic.
Key points:
- All nodes use the same MESH_PREFIX, MESH_PASSWORD, and MESH_PORT
mesh.sendBroadcast()sends to every node in the meshmesh.getNodeId()returns a unique uint32_t identifier derived from the MAC address- The
userSchedulerintegrates with painlessMesh's internal task system
Sensor Mesh: Broadcasting Sensor Data
In a real deployment, each node reads local sensors and shares the data across the mesh. Here each node reads a DHT22 temperature/humidity sensor and broadcasts the readings as JSON.
#include <painlessMesh.h>
#include <ArduinoJson.h>
#include <DHT.h>
#define MESH_PREFIX "wavtronMesh"
#define MESH_PASSWORD "meshPassword123"
#define MESH_PORT 5555
#define DHT_PIN 4
#define DHT_TYPE DHT22
painlessMesh mesh;
Scheduler userScheduler;
DHT dht(DHT_PIN, DHT_TYPE);
Task taskReadSensor(TASK_SECOND * 10, TASK_FOREVER, []() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if (isnan(temp) || isnan(hum)) {
Serial.println("DHT read failed");
return;
}
JsonDocument doc;
doc["node_id"] = mesh.getNodeId();
doc["temperature"] = round(temp * 10.0) / 10.0;
doc["humidity"] = round(hum * 10.0) / 10.0;
doc["uptime_s"] = millis() / 1000;
String output;
serializeJson(doc, output);
mesh.sendBroadcast(output);
Serial.printf("Sent: T=%.1f C, H=%.1f%%\n", temp, hum);
});
void receivedCallback(uint32_t from, String &msg) {
JsonDocument doc;
deserializeJson(doc, msg);
Serial.printf("Node %u -> T:%.1f C H:%.1f%%\n",
(uint32_t)doc["node_id"],
(float)doc["temperature"],
(float)doc["humidity"]);
}
void setup() {
Serial.begin(115200);
dht.begin();
mesh.setDebugMsgTypes(ERROR | STARTUP);
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
mesh.onReceive(&receivedCallback);
userScheduler.addTask(taskReadSensor);
taskReadSensor.enable();
}
void loop() {
mesh.update();
}
Root Node: Collecting Data and Forwarding to MQTT
The bridge node pattern is how you connect your mesh to the internet. One ESP32 acts as the root node — it joins the mesh and connects to your WiFi router, forwarding collected data to an MQTT broker.
#include <painlessMesh.h>
#include <ArduinoJson.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#define MESH_PREFIX "wavtronMesh"
#define MESH_PASSWORD "meshPassword123"
#define MESH_PORT 5555
#define WIFI_SSID "YourHomeWiFi"
#define WIFI_PASS "YourWiFiPassword"
#define MQTT_BROKER "192.168.1.100"
#define MQTT_PORT 1883
painlessMesh mesh;
Scheduler userScheduler;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
// painlessMesh normally controls WiFi. As root, we let it connect
// to the actual router using stationManual.
void receivedCallback(uint32_t from, String &msg) {
Serial.printf("Mesh -> %u: %s\n", from, msg.c_str());
// Forward every mesh message to MQTT
String topic = "mesh/node/";
topic += from;
mqtt.publish(topic.c_str(), msg.c_str());
}
void reconnectMQTT() {
if (!mqtt.connected()) {
Serial.print("Connecting MQTT...");
if (mqtt.connect("mesh-root")) {
Serial.println("connected");
} else {
Serial.printf("failed, rc=%d\n", mqtt.state());
}
}
}
Task taskMQTT(TASK_SECOND * 5, TASK_FOREVER, []() {
reconnectMQTT();
});
void setup() {
Serial.begin(115200);
// Configure mesh
mesh.setDebugMsgTypes(ERROR | STARTUP);
mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT);
mesh.onReceive(&receivedCallback);
// This node is the root — it connects to the real WiFi router
mesh.stationManual(WIFI_SSID, WIFI_PASS);
mesh.setHostname("mesh-root");
// Designate this node as the root
mesh.setRoot(true);
// Root should always try to connect to the router
mesh.setContainsRoot(true);
// MQTT setup
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
userScheduler.addTask(taskMQTT);
taskMQTT.enable();
}
void loop() {
mesh.update();
mqtt.loop();
}
Important: Call mesh.setContainsRoot(true) on all nodes in the mesh (not just the root) so they know to organize the tree topology with the root node at the top.
Targeted Messaging: Sending to a Specific Node
Broadcasting works fine for sensor data, but sometimes you need to send a command to one specific node — for example, turning on a relay.
// Send to a specific node by its ID
uint32_t targetNodeId = 2835076412; // Get this from Serial Monitor output
JsonDocument cmd;
cmd["action"] = "relay_on";
cmd["pin"] = 25;
String output;
serializeJson(cmd, output);
bool sent = mesh.sendSingle(targetNodeId, output);
Serial.printf("Sent to %u: %s (%s)\n",
targetNodeId, output.c_str(),
sent ? "delivered" : "failed");
On the receiving end, parse the command in your receivedCallback:
void receivedCallback(uint32_t from, String &msg) {
JsonDocument doc;
deserializeJson(doc, msg);
if (doc["action"] == "relay_on") {
int pin = doc["pin"];
digitalWrite(pin, HIGH);
Serial.printf("Relay ON (pin %d) — commanded by %u\n", pin, from);
}
}
Mesh Topology: How Nodes Discover and Connect
When a new ESP32 powers on with painlessMesh, it:
- Scans for WiFi networks matching MESH_PREFIX
- Evaluates available parent nodes based on signal strength and layer depth
- Connects to the best parent node
- Announces itself to the mesh
- Begins accepting child connections
The resulting topology looks like this:
[Router]
|
[Root Node] <-- Layer 0 (internet bridge)
/ \
[Node A] [Node B] <-- Layer 1
/ \ \
[Node C] [Node D] [Node E] <-- Layer 2
If Node A goes offline, Nodes C and D detect the disconnection and automatically reconnect through Node B or directly to the Root (if in range). This self-healing takes 5-30 seconds depending on the network size.
Performance: What to Expect
Based on real-world testing with ESP32-DevKitC boards:
| Metric | Typical Value | Notes |
|---|---|---|
| Node-to-node latency | 5-50 ms | Per hop; increases with each layer |
| End-to-end latency (4 hops) | 50-200 ms | Acceptable for sensor data, not for real-time control |
| Maximum tested nodes | 30-50 | painlessMesh works reliably up to ~50 nodes |
| Theoretical max (ESP-MESH) | ~1000 | Espressif's claim; practical limit is much lower |
| Throughput per link | ~1 Mbps | Mesh overhead reduces raw WiFi throughput |
| Message size limit | ~1.5 KB | Larger messages get fragmented and may fail |
| Reconnection time | 5-30 seconds | After a node drops out |
| Range per hop (indoor) | 15-30 metres | Through walls; open space is better |
| Range per hop (outdoor) | 50-150 metres | Line of sight with stock antenna |
Scaling note: Beyond 30 nodes, you will notice increased latency and occasional topology reshuffles. For networks of 50+ nodes, consider splitting into multiple mesh networks with individual bridge nodes feeding a common MQTT broker.
ESP-NOW: The Simpler Alternative
ESP-NOW is Espressif's lightweight protocol for direct device-to-device communication. It works at the data link layer — no TCP/IP stack, no connection handshake, no router needed.
Key differences from painlessMesh:
- No mesh routing: messages go directly from sender to receiver (single hop)
- Much faster: ~2-5 ms latency (vs 5-50 ms per hop in mesh)
- Lower overhead: no topology management, no task scheduler
- Simpler code: fewer callbacks, less RAM usage
- Limited range: no multi-hop relay; only as far as one ESP32 can transmit directly
ESP-NOW is ideal when all your nodes can reach either each other or one central receiver directly.
ESP-NOW: Sender
#include <esp_now.h>
#include <WiFi.h>
// MAC address of the receiver ESP32
// Find it by running WiFi.macAddress() on the receiver
uint8_t receiverMAC[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
typedef struct {
uint32_t nodeId;
float temperature;
float humidity;
uint32_t timestamp;
} SensorPacket;
void onDataSent(const uint8_t *mac, esp_now_send_status_t status) {
Serial.printf("Send status: %s\n",
status == ESP_NOW_SEND_SUCCESS ? "OK" : "FAIL");
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
return;
}
esp_now_register_send_cb(onDataSent);
// Register the receiver as a peer
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, receiverMAC, 6);
peerInfo.channel = 0; // Use current channel
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add peer");
return;
}
}
void loop() {
SensorPacket packet;
packet.nodeId = 1;
packet.temperature = 28.5; // Replace with actual sensor read
packet.humidity = 65.0;
packet.timestamp = millis();
esp_err_t result = esp_now_send(receiverMAC,
(uint8_t *)&packet,
sizeof(packet));
if (result == ESP_OK) {
Serial.println("Packet sent");
} else {
Serial.println("Send error");
}
delay(5000);
}
ESP-NOW: Receiver (Central Collector)
#include <esp_now.h>
#include <WiFi.h>
typedef struct {
uint32_t nodeId;
float temperature;
float humidity;
uint32_t timestamp;
} SensorPacket;
void onDataReceived(const esp_now_recv_info_t *info,
const uint8_t *data, int len) {
if (len != sizeof(SensorPacket)) return;
SensorPacket packet;
memcpy(&packet, data, sizeof(packet));
Serial.printf("Node %u -> T:%.1f C H:%.1f%% (age: %lums)\n",
packet.nodeId,
packet.temperature,
packet.humidity,
millis() - packet.timestamp);
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
Serial.printf("Receiver MAC: %s\n", WiFi.macAddress().c_str());
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
return;
}
esp_now_register_recv_cb(onDataReceived);
Serial.println("Waiting for data...");
}
void loop() {
// Nothing needed — data arrives via callback
delay(100);
}
ESP-NOW supports up to 20 peers per device (6 encrypted, 14 unencrypted on most ESP-IDF versions). For a star topology with one receiver and multiple senders, only the senders need the receiver's MAC — the receiver accepts from any registered or broadcast source.
ESP-NOW vs painlessMesh vs BLE Mesh: Comparison
| Feature | ESP-NOW | painlessMesh | BLE Mesh |
|---|---|---|---|
| Topology | Star / Point-to-point | Tree mesh | Full mesh |
| Multi-hop routing | No | Yes | Yes |
| Self-healing | No | Yes | Yes |
| Latency (per hop) | 2-5 ms | 5-50 ms | 10-100 ms |
| Range per hop | 30-150 m | 15-150 m | 10-30 m |
| Max nodes | 20 peers per device | ~50 practical | ~30 practical |
| Bandwidth | ~1 Mbps | ~1 Mbps | ~125 Kbps |
| Max packet size | 250 bytes | ~1.5 KB | 384 bytes |
| Power consumption | Low (can deep sleep) | High (must stay awake) | Low |
| Internet bridge | Manual | Built-in (root node) | Manual |
| Encryption | AES-128 (built-in) | WPA2 (mesh WiFi) | AES-128 (built-in) |
| Complexity | Simple | Moderate | Complex |
| Best for | Few nodes, fast data, deep sleep | Large area coverage, sensor networks | Low-power wearables, BLE ecosystem |
Rule of thumb:
- Up to 10 nodes, all within range of one collector: use ESP-NOW
- 10-50 nodes, spread across a large area: use painlessMesh
- Battery-powered nodes needing months of runtime: use BLE Mesh or ESP-NOW with deep sleep
Use Cases in India
Large Home Automation
Indian bungalows and multi-storey homes with thick walls are perfect candidates for mesh. Place an ESP32 node in each room for temperature, motion, and light sensing. The mesh relays data through walls that would block a single WiFi signal. Root node in the living room connects to your home router and feeds data to Home Assistant.
Agricultural Sensor Networks
Monitor soil moisture, temperature, and humidity across a farm. ESP32 nodes at 50-metre intervals with external antennas can cover several acres. The mesh topology means you only need internet connectivity (a 4G router or a phone hotspot) at one edge of the field. Particularly relevant for precision agriculture initiatives gaining traction across Indian states.
Factory Floor Monitoring
Manufacturing units in industrial areas like Peenya (Bangalore), Manesar (Gurugram), or MIDC (Pune) often have metal structures and heavy machinery causing severe WiFi interference. A mesh network with nodes mounted at each workstation can monitor machine vibration, temperature, and operating status, routing around obstacles automatically.
Event and Exhibition Installations
Large venues like convention centres, college fests, or exhibition halls need temporary sensor/display networks. Mesh networking lets you deploy 20-30 ESP32 nodes across a venue in minutes without coordinating with the venue's IT infrastructure. When the event ends, you pack up the nodes and leave.
Power Considerations
This is the biggest limitation of ESP-MESH and painlessMesh: mesh nodes must stay awake. Each node needs to be available to relay messages for other nodes at any time. Deep sleep breaks the mesh because sleeping nodes cannot forward traffic.
Practical power options:
- Mains power: Ideal for fixed installations. Use a small 5V adapter at each node location.
- USB power banks: Good for temporary setups. A 10,000 mAh power bank runs an ESP32 mesh node for 30-40 hours.
- Solar + battery: For outdoor agricultural deployments. A small 6V solar panel with a TP4056 charger and 18650 cell works well in Indian sunshine.
- ESP-NOW with deep sleep: If you can sacrifice mesh routing, ESP-NOW senders can deep sleep between transmissions and achieve months of battery life.
Common Issues and Solutions
Network Partitioning
If a critical relay node fails and the nodes behind it cannot reach any other parent, the network splits into two disconnected groups. Solution: Ensure physical overlap between nodes so that every node can reach at least two other nodes.
High Latency with Many Hops
Each hop adds 5-50 ms of latency. With 5+ hops, you might see 200-500 ms delays. Solution: Keep the tree shallow. Place the root node centrally. Add more nodes to create shorter paths rather than extending a long chain.
WiFi Channel Conflicts
painlessMesh operates on a specific WiFi channel. If your router or neighbouring networks use the same channel, interference increases. Solution: Set a fixed channel in mesh.init() that does not conflict with your environment. Use a WiFi analyzer app to find the least congested channel.
Memory Pressure
With many nodes sending JSON messages, the ESP32's RAM can fill up. Solution: Keep messages compact. Use short key names. Avoid sending messages faster than the mesh can deliver them. A good rule is one message per node every 10-30 seconds.
Security: Encrypting Mesh Messages
painlessMesh uses WPA2 encryption on the WiFi layer — the mesh network itself is password-protected (your MESH_PASSWORD). However, for sensitive data, you should add application-layer encryption.
A lightweight approach using AES-128:
#include <AES.h> // From the "Crypto" library
#include <base64.h>
// Shared 16-byte key — same on all nodes
const uint8_t aesKey[16] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
};
AES128 aes;
String encryptMessage(const String &plaintext) {
// Pad to 16-byte blocks
int len = plaintext.length();
int paddedLen = ((len / 16) + 1) * 16;
uint8_t padded[paddedLen];
memset(padded, 0, paddedLen);
memcpy(padded, plaintext.c_str(), len);
// Encrypt each block (ECB mode for simplicity)
uint8_t encrypted[paddedLen];
aes.setKey(aesKey, 16);
for (int i = 0; i < paddedLen; i += 16) {
aes.encryptBlock(encrypted + i, padded + i);
}
return base64::encode(encrypted, paddedLen);
}
For production systems, consider AES-CBC with a random IV prepended to each message for stronger security.
Scaling Tips for Large Networks
When you move beyond a dozen nodes, follow these guidelines:
-
Centralise the root node physically. Place it in the middle of your deployment area so the tree stays shallow and balanced.
-
Use
mesh.setContainsRoot(true)on every node. This ensures the tree organises itself correctly with the bridge node at the top. -
Limit broadcast frequency. With 30 nodes each broadcasting every 5 seconds, you generate 6 messages per second across the entire mesh. Increase intervals to 15-30 seconds for large networks.
-
Keep payloads small. Aim for under 200 bytes per message. Use short JSON keys (
"t"instead of"temperature") or switch to a binary format like MessagePack. -
Monitor topology. painlessMesh provides
mesh.subConnectionJson()which returns the entire network topology as JSON. Log this periodically to detect issues. -
Use fixed root selection. Set
mesh.setRoot(true)on your bridge node rather than letting the mesh auto-elect a root. Auto-election can cause unnecessary topology reshuffles. -
Separate mesh and internet traffic. The root node handles both mesh management and router communication. On very busy networks, consider using two ESP32s — one for mesh and one for internet — connected via serial.
-
Add external antennas. The onboard PCB antenna has limited range. For outdoor deployments or large indoor spaces, use ESP32 modules with an IPEX connector and attach a 2.4 GHz external antenna to double or triple your range per hop.
What You Need to Get Started
For a basic three-node mesh, you will need:
- 3x ESP32-DevKitC boards — any ESP32 variant with WiFi works
- 3x USB cables for programming and power
- 3x DHT22 sensors (optional, for the sensor mesh example)
- Jumper wires and a breadboard for sensor connections
- Arduino IDE or PlatformIO with ESP32 board support installed
For the root/bridge node with MQTT, you will also need:
- A computer or Raspberry Pi running an MQTT broker (Mosquitto is free and lightweight)
- A WiFi router for the root node to connect to
Wrapping Up
ESP32 mesh networking solves a real problem that every IoT builder encounters eventually: getting data from many distributed nodes to one place without running cables or depending on WiFi coverage everywhere.
Start with painlessMesh if you need multi-hop routing across large areas. Its self-healing topology and built-in bridge pattern make it the most practical choice for sensor networks, building automation, and industrial monitoring.
Start with ESP-NOW if your nodes are all within range of one central receiver and you need speed, simplicity, or battery-powered operation with deep sleep.
Either way, the ESP32 gives you the tools to build networks that commercial mesh solutions charge thousands of rupees for — using boards that cost a few hundred rupees each.
Flash the basic mesh example onto three boards, watch them discover each other in your Serial Monitor, and build from there. The code in this guide is production-tested and ready to adapt to your specific use case.



