ESP-NOW Protocol

Technical reference for ESP-NOW — Espressif's peer-to-peer protocol for direct ESP32 communication without a WiFi router

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

ParameterValue
Max payload per message$250\,\text{bytes}$
Max encrypted peers10 (Station mode) / 20 (SoftAP or SoftAP+Station mode)
Max unencrypted peers20
Latency~$1\,\text{ms}$ (typical)
Range~$200\,\text{m}$ line of sight (open air, default TX power)
EncryptionCCMP (AES-128), optional per peer
Frequency$2.4\,\text{GHz}$ (same as WiFi)
Requires WiFi connectionNo -- uses WiFi radio directly
AcknowledgmentBuilt-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-NOWWiFi + MQTT
Latency~$1\,\text{ms}$~$50$--$200\,\text{ms}$
Router requiredNoYes
Internet requiredNoDepends on broker location
Max payload$250\,\text{bytes}$Limited by broker/library (PubSubClient: $256\,\text{bytes}$ default, configurable)
Range~$200\,\text{m}$ LoSDepends on WiFi coverage
Number of devices20 peers maxHundreds+ (broker handles routing)
Power consumptionLower (no connection overhead)Higher (TCP stack, keep-alive)
Data persistenceNoneBroker can retain messages
Delivery guaranteeACK 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

ModeTarget MACBehavior
UnicastSpecific MAC addressMessage sent to one peer; delivery callback reports success or failure
BroadcastFF:FF:FF:FF:FF:FFMessage 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.

ScenarioWorks?Notes
ESP-NOW only (no WiFi)YesUse WiFi.mode(WIFI_STA) but do not call WiFi.begin()
ESP-NOW + WiFi StationYesESP-NOW locked to the router's channel
ESP-NOW + SoftAPYesESP-NOW uses the AP's channel
Two ESP-NOW nodes on different channelsNoMessages 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

ProblemCauseFix
Send callback reports FAILMAC address wrongDouble-check receiver's MAC with WiFi.macAddress()
Send callback reports FAILChannel mismatchEnsure both boards are on the same WiFi channel
No data receivedReceiver callback not registeredCall esp_now_register_recv_cb() in setup()
Garbled dataStruct mismatch between sender and receiverUse identical struct definitions on both boards
Payload too largeExceeded $250\,\text{byte}$ limitReduce struct size or split into multiple messages
ESP-NOW stops working after WiFi connectChannel changed when connecting to routerSet peer channel to 0 (auto) and re-add peer after WiFi connects
Only 10 encrypted peersStation mode limitSwitch 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 typeSize
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: