ESP-NOW is a connectionless communication protocol developed by Espressif. It lets ESP32 boards talk directly to each other using their WiFi radios -- without connecting to a WiFi network, without a router, and without the internet. Messages arrive in roughly $1\,\text{ms}$, making it one of the fastest ways to get data between two ESP32 boards.
Think of it like a walkie-talkie: you address a specific device (by MAC address), press "send," and the message arrives almost instantly. No infrastructure required.
🔗Key Specs
| Parameter | Value |
|---|---|
| Max payload per message | $250\,\text{bytes}$ |
| Max encrypted peers | 10 (Station mode) / 20 (SoftAP or SoftAP+Station mode) |
| Max unencrypted peers | 20 |
| Latency | ~$1\,\text{ms}$ (typical) |
| Range | ~$200\,\text{m}$ line of sight (open air, default TX power) |
| Encryption | CCMP (AES-128), optional per peer |
| Frequency | $2.4\,\text{GHz}$ (same as WiFi) |
| Requires WiFi connection | No -- uses WiFi radio directly |
| Acknowledgment | Built-in delivery callback (success/fail) |
🔗How It Works
ESP-NOW operates at the data link layer (Layer 2), below IP. Instead of IP addresses, it uses MAC addresses to identify peers. When you send a message, the WiFi radio transmits a vendor-specific action frame -- no association, no TCP handshake, no IP routing.
graph TD
A[ESP32 Node A] <-->|ESP-NOW<br>MAC-based| B[ESP32 Node B]
A <-->|ESP-NOW| C[ESP32 Node C]
B <-->|ESP-NOW| C
B <-->|ESP-NOW| D[ESP32 Node D]Each node can be a sender, a receiver, or both. There is no central broker or coordinator -- any node can send to any other node it has registered as a peer.
🔗ESP-NOW vs WiFi + MQTT
| ESP-NOW | WiFi + MQTT | |
|---|---|---|
| Latency | ~$1\,\text{ms}$ | ~$50$--$200\,\text{ms}$ |
| Router required | No | Yes |
| Internet required | No | Depends on broker location |
| Max payload | $250\,\text{bytes}$ | Limited by broker/library (PubSubClient: $256\,\text{bytes}$ default, configurable) |
| Range | ~$200\,\text{m}$ LoS | Depends on WiFi coverage |
| Number of devices | 20 peers max | Hundreds+ (broker handles routing) |
| Power consumption | Lower (no connection overhead) | Higher (TCP stack, keep-alive) |
| Data persistence | None | Broker can retain messages |
| Delivery guarantee | ACK per message (success/fail callback) | QoS 0/1/2 |
Use ESP-NOW when you need fast, direct communication between a small number of ESP32 boards without infrastructure. Use MQTT when you need internet connectivity, dashboards, many devices, or message persistence.
🔗Getting the MAC Address
Every ESP32 has a unique MAC address. You need it to register peers. This sketch prints the MAC address to the Serial Monitor:
#include <WiFi.h>
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
Serial.print("MAC Address: ");
Serial.println(WiFi.macAddress());
}
void loop() {}The output looks like 24:6F:28:A1:B2:C3. Write down the MAC address of each board you plan to use.
🔗Broadcast vs Unicast
| Mode | Target MAC | Behavior |
|---|---|---|
| Unicast | Specific MAC address | Message sent to one peer; delivery callback reports success or failure |
| Broadcast | FF:FF:FF:FF:FF:FF | Message sent to all ESP-NOW devices on the same channel; no delivery confirmation |
Broadcast is useful for discovery or when you want all nearby nodes to receive a reading. Unicast is more reliable because the sender gets an acknowledgment.
🔗Code Example: Sender and Receiver
Below are two short sketches -- one for the sending ESP32 and one for the receiver. Replace the MAC address in the sender with the actual MAC address of your receiver board.
🔗Sender
#include <WiFi.h>
#include <esp_now.h>
// Replace with your RECEIVER's MAC address
uint8_t receiverMAC[] = {0x24, 0x6F, 0x28, 0xA1, 0xB2, 0xC3};
// Define a struct for the data (must match receiver)
typedef struct {
float temperature;
float humidity;
uint32_t sequence;
} SensorData;
SensorData outgoing;
uint32_t seq = 0;
// Callback: called after each send attempt
void onSent(const uint8_t *mac, esp_now_send_status_t status) {
Serial.printf("Send to %02X:%02X:%02X:%02X:%02X:%02X — %s\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
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(onSent);
// Register the receiver as a peer
esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, receiverMAC, 6);
peer.channel = 0; // Use current WiFi channel
peer.encrypt = false;
if (esp_now_add_peer(&peer) != ESP_OK) {
Serial.println("Failed to add peer");
}
}
void loop() {
outgoing.temperature = 23.5;
outgoing.humidity = 61.0;
outgoing.sequence = seq++;
esp_now_send(receiverMAC, (uint8_t *)&outgoing, sizeof(outgoing));
delay(2000);
}🔗Receiver
#include <WiFi.h>
#include <esp_now.h>
// Must match the sender's struct exactly
typedef struct {
float temperature;
float humidity;
uint32_t sequence;
} SensorData;
// Callback: called when a message is received
void onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
if (len != sizeof(SensorData)) {
Serial.println("Unexpected message size");
return;
}
SensorData incoming;
memcpy(&incoming, data, sizeof(incoming));
Serial.printf("#%lu Temp: %.1f°C Hum: %.1f%% from %02X:%02X:%02X:%02X:%02X:%02X\n",
incoming.sequence,
incoming.temperature,
incoming.humidity,
info->src_addr[0], info->src_addr[1], info->src_addr[2],
info->src_addr[3], info->src_addr[4], info->src_addr[5]);
}
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_recv_cb(onReceive);
Serial.println("Receiver ready. Waiting for messages...");
}
void loop() {
// Nothing here -- onReceive is called automatically
}Upload the receiver sketch to one ESP32 and the sender sketch to another (after updating the MAC address). Open the receiver's Serial Monitor and you should see data arriving every $2\,\text{s}$.
Struct alignment: Both boards must use the same struct layout. If you change the struct on one side, update the other side to match. Adding or removing fields without recompiling both sketches will cause garbled data.
🔗Coexistence with WiFi
ESP-NOW and WiFi can run simultaneously on the same ESP32, but there is one important constraint: both must use the same WiFi channel. If your WiFi router is on channel 6, your ESP-NOW communication must also be on channel 6.
| Scenario | Works? | Notes |
|---|---|---|
| ESP-NOW only (no WiFi) | Yes | Use WiFi.mode(WIFI_STA) but do not call WiFi.begin() |
| ESP-NOW + WiFi Station | Yes | ESP-NOW locked to the router's channel |
| ESP-NOW + SoftAP | Yes | ESP-NOW uses the AP's channel |
| Two ESP-NOW nodes on different channels | No | Messages will not be received |
When using ESP-NOW without WiFi, the default channel is typically 1. You can change it with esp_wifi_set_channel().
🔗Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Send callback reports FAIL | MAC address wrong | Double-check receiver's MAC with WiFi.macAddress() |
| Send callback reports FAIL | Channel mismatch | Ensure both boards are on the same WiFi channel |
| No data received | Receiver callback not registered | Call esp_now_register_recv_cb() in setup() |
| Garbled data | Struct mismatch between sender and receiver | Use identical struct definitions on both boards |
| Payload too large | Exceeded $250\,\text{byte}$ limit | Reduce struct size or split into multiple messages |
| ESP-NOW stops working after WiFi connect | Channel changed when connecting to router | Set peer channel to 0 (auto) and re-add peer after WiFi connects |
| Only 10 encrypted peers | Station mode limit | Switch to SoftAP+Station mode for up to 20 encrypted peers |
🔗Message Size Budget
With a $250\,\text{byte}$ limit, plan your data structure carefully:
| Data type | Size |
|---|---|
float | $4\,\text{bytes}$ |
int32_t | $4\,\text{bytes}$ |
uint8_t | $1\,\text{byte}$ |
bool | $1\,\text{byte}$ |
char[32] (string) | $32\,\text{bytes}$ |
A struct with 5 floats, 2 ints, and a 32-byte string uses $5 \times 4 + 2 \times 4 + 32 = 60\,\text{bytes}$ -- well within the limit. You can fit a substantial amount of sensor data in a single message, but sending raw image data or large text payloads is not feasible.
🔗Used In
The following pages on this site use ESP-NOW:
- Wireless Sensor Network -- multiple ESP32 nodes communicating via ESP-NOW