Smart devices such as fitness trackers and beacons rely heavily on Bluetooth Low Energy (BLE) to communicate wirelessly without draining their batteries. For people building these electronics, the ESP32 chip is a highly popular choice because it packs both Wi-Fi and Bluetooth into one tiny piece of hardware.
Typically, a built-in software package called Bluedroid is used to handle Bluetooth on the ESP32. While Bluedroid is powerful and supports both Bluetooth Classic and Bluetooth Low Energy, it is quite heavy. It uses up a massive amount of the chip’s storage memory and active processing power because it’s designed to handle the complex state machines of both Bluetooth Classic and Bluetooth Low Energy concurrently. If your project only needs Bluetooth Low Energy, using the massive Bluedroid system is entirely unnecessary.
This issue led to the rise of NimBLE, a completely open-source, highly optimized Bluetooth Low Energy stack originally developed under the Apache Mynewt real-time operating system project. Switching from the heavy Bluedroid framework to the lightweight NimBLE stack will not only reduce memory overhead but also ensure your ESP32 devices operate with maximum efficiency.
This guide explores why NimBLE is a better choice than Bluedroid, explains how to set it up in your Arduino software, and walks you through deploying your first basic NimBLE Bluetooth server.
Let’s get started!
Bluedroid versus NimBLE
To understand why NimBLE is useful, you must understand the fundamental structural differences between the NimBLE stack and the standard Bluedroid-based ESP32 implementation.
Consumption of Flash and SRAM
The main issue with Bluedroid is that it’s designed to handle the complex tasks of Bluetooth Classic simultaneously with Bluetooth Low Energy (BLE). Even if you turn off the classic Bluetooth features in your code, the hidden, unoptimized background code is still there, eating up valuable space.
NimBLE, on the other hand, was designed from its inception as a BLE-exclusive stack. Because it drops vast amounts of unnecessary protocol handling logic, it takes up far less permanent storage space and uses significantly less active memory while running.
Runtime efficiency and responsiveness
Beyond just saving space, NimBLE is also much faster and more responsive. This is important for projects that use deep sleep. In deep sleep, the ESP32 turns off most of its parts to save battery power. When it wakes up, it needs to start Bluetooth, broadcast a piece of data, and then go back to sleep as quickly as possible.
Tests show that NimBLE usually starts much faster than Bluedroid. For example, Bluedroid may take about 631 milliseconds to initialize, while NimBLE may take about 209 milliseconds. That is a big improvement.
Here is a simple comparison:
| Operation Phase | Bluedroid Average Latency | NimBLE Average Latency | Performance Delta |
| Stack Initialization | 631 milliseconds | 209 milliseconds | 66.8% reduction |
| Client Connection | 316 milliseconds | 414 milliseconds | 31.0% increase |
| Total Elapsed Time | 946 milliseconds | 623 milliseconds | 34.1% reduction |
Although NimBLE may take a little longer to negotiate and establish the physical connection with a peer device, it starts so much faster that the total time is still shorter. This can help save battery life because the ESP32 spends less time awake.
Density of concurrent connections
NimBLE can also handle more BLE connections at the same time. While the Bluedroid stack typically limits the ESP32 to three to seven simultaneous connections depending on the settings, NimBLE natively supports up to nine concurrent BLE connections.
This extra capacity is incredibly helpful if you are building a central hub that needs to gather data from many different sensors simultaneously.
Basics of Bluetooth Low Energy (BLE)
Before we create our ESP32 NimBLE server, you should be familiar with the basics of Bluetooth Low Energy.
In our upcoming code, we are going to configure the ESP32 to act as a server. We will set it up to broadcast a specific service, and inside that service, we will create a characteristic that holds our actual data value. To make sure other devices know exactly what this data is, we will assign everything a unique identification number called a UUID. If any of those terms sound a bit foreign to you, do not worry. Let’s quickly walk through how Bluetooth data is structured so the code makes perfect sense.
When working with BLE, everything revolves around two main systems known as GAP and GATT.
GAP (Generic Access Profile)
GAP controls connection and advertising phase in Bluetooth. GAP is what makes your device visible to the outside world and determines how two devices discover and establish a connection with each other.
GAP defines various roles for devices, but the two key concepts to keep in mind are Central devices and Peripheral devices.
Peripheral devices are usually small, low-power, resource-constrained devices, such as heart rate monitors or proximity tags, that can connect to a much more powerful central device.
Central devices are usually devices with far more processing power and memory, such as mobile phones or tablets, to which you connect.

A peripheral device advertises by sending out advertising packets at set intervals to inform nearby central devices of its presence. Once a connection is established between a peripheral and a central device, the advertising process stops and GATT comes into play.
GATT (Generic ATTribute Profile)
GATT specifies how data should be organized and how two BLE devices should share data. Unlike GAP, which defines the low-level interactions with devices, GATT deals only with actual data transfer procedures and formats.
The data is organized hierarchically into sections called services, which group conceptually related pieces of user data called characteristics, as shown in the illustration below:

Services
A GATT service is a collection of conceptually related data called characteristics. Each service can have one or more characteristics and has its own unique numeric identifier, or UUID, which is either 16 bits (for officially adopted BLE Services) or 128 bits (for custom services). More on that later.
For instance, let’s consider the Heart Rate Service. This officially adopted service has a 16-bit UUID of 0x180D, and contains up to 3 characteristics: Heart Rate Measurement, Body Sensor Location and Heart Rate Control Point. You can find more Bluetooth SIG-defined services here.
Characteristics
A GATT characteristic is a group of information called Attributes. Attributes are the information actually transferred between BLE devices. A typical characteristic is composed of the following attributes:

- Value: is the actual data that is stored in the characteristic. The value can be any type of data, such as a number, a string, or an array of bytes.
- Descriptor: provides additional information or configuration options for the characteristic.
In addition to the value, the following properties are associated with each characteristic:
- Handle: a 16-bit number used to access the characteristic on the server device.
- UUID: a 128-bit universally unique identifier that indicates what the characteristic represents.
- Permissions: defines which operations on the characteristic are permitted, such as read, write, or notify.
UUID (Universally Unique Identifier)
UUIDs ensure that services and characteristics can be uniquely identified. They play an essential role in defining and accessing the data on a BLE device. There are two types of UUIDs in BLE:
- 16-bit UUID: is used for official BLE profiles, services, and characteristics. These are standardized by the Bluetooth-SIG. For example, the “Heart Rate Service” has a standardized 16-bit UUID of 0x180D, while the “Heart Rate Measurement” characteristic within the Heart Rate Service uses a UUID of 0x2A37.
- 128-bit UUID: is used for custom (vendor-specific) services and characteristics. If a company develops its own service not covered by the official BLE services, they would use a unique 128-bit UUID. A 128-bit UUID looks like this: 4fafc201-1fb5-459e-8fcc-c5c9c331914b.
GATT Server and GATT Client
From a GATT perspective, when two devices are connected, they are each in one of two roles.
- The GATT server is the device that contains the characteristic database.
- The GATT client is the device that reads from or writes data to the GATT server.
The following figure illustrates this relationship in a sample BLE connection, where the peripheral device (an ESP32) is the GATT server holding the data, while the central device (a smartphone) is the GATT client that reads it.

Creating a Simple ESP32 NimBLE Server
Now that you know exactly what a GATT server, a service, and a characteristic actually are, it is time to build one.
To make this process as easy as possible, we will be using the NimBLE-Arduino library. Developed by Ryan Powell, this library takes the lightning-fast, lightweight NimBLE system and makes it easy to compile and run in the Arduino IDE. Its main goal is to provide a lighter alternative to the original Bluedroid-based ESP32 BLE library by Neil Kolban, while keeping the programming commands almost the same. This makes it much easier for developers to upgrade their older BLE code to NimBLE.
To install the library,
- First open your Arduino IDE program. Then click on the Library Manager icon on the left sidebar.
- Type “nimble” in the search box to filter your results.
- Look for the NimBLE-Arduino library by h2zero.
- Click the Install button to add it to your Arduino IDE.

Arduino Sketch
The example below sets up the ESP32 as a NimBLE server. The ESP32 advertises itself so nearby BLE devices can discover it. It also creates one BLE service and one BLE characteristic. A client device, such as a smartphone, can connect to the ESP32 and read the value stored inside the characteristic.
#include <NimBLEDevice.h>
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
void setup() {
// Initialize the primary serial interface for debugging output
Serial.begin(115200);
Serial.println("Starting NimBLE Server Configuration...");
// Phase 1: Initialize the BLE stack with a unique device name
NimBLEDevice::init("MyESP32");
// Phase 2: Instantiate the Server and create the Service
NimBLEServer *pServer = NimBLEDevice::createServer();
NimBLEService *pService = pServer->createService(SERVICE_UUID);
// Phase 3: Create the Characteristic and define its properties
NimBLECharacteristic *pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
NIMBLE_PROPERTY::READ |
NIMBLE_PROPERTY::WRITE
);
// Assign an initial payload to the characteristic
pCharacteristic->setValue("Hello World!");
// Phase 4: Start the service to make it accessible
pService->start();
// Phase 5: Configure and initiate advertising
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->setName("MyESP32");
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->enableScanResponse(true);
pAdvertising->start();
Serial.println("NimBLE Server is successfully advertising.");
}
void loop() {
// The FreeRTOS scheduler handles BLE tasks in the background;
// the main loop can remain empty or handle other sensor logic.
delay(2000);
}Code Explanation
Let’s go over the code in detail. It starts by including the NimBLEDevice library for BLE operations on the ESP32.
#include <NimBLEDevice.h>Next, we define the UUIDs (Universally Unique Identifiers), for our Service and Characteristic. These are special ID tags that help other devices recognize exactly what kind of data we are offering. If you want to use your own unique IDs instead of the default ones provided here, you can easily generate new ones using an online UUID generator.
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"Inside the setup function, we first open the serial connection at a baud rate of 115200.
Serial.begin(115200);After that, we initialize the BLE stack using the init() method and pass it the name "MyESP32". This string is simply the public name your ESP32 will broadcast to the world. This way, when you pull out your phone to scan for nearby Bluetooth devices, you’ll know exactly which one is yours. You can change this name to anything you like to make your device stand out in a crowded Bluetooth scan.
NimBLEDevice::init("MyESP32");Once the BLE stack is initialized, we create the actual server that will handle all incoming connections.
NimBLEServer *pServer = NimBLEDevice::createServer();Immediately after creating the server, we create a new service inside it and attach the service UUID we defined earlier.
NimBLEService *pService = pServer->createService(SERVICE_UUID);Next, we create a characteristic inside that service. A characteristic is where the actual value is stored. When we set this up, we pass in our characteristic UUID along with a specific set of permissions. We assign the READ and WRITE properties to dictate exactly what connected devices are allowed to do. With READ, the phone can read the value from the ESP32. With WRITE, the phone can send a new value to the ESP32.
We combine these properties using the | symbol. This symbol lets us join multiple options together. So instead of giving the characteristic only one ability, we give it two abilities: read and write.
Note that if you ever forget to grant these permissions, your connected devices will run into frustrating errors like “GATT WRITE NOT ALLOWED” when they try to interact with your ESP32.
NimBLECharacteristic *pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
NIMBLE_PROPERTY::READ |
NIMBLE_PROPERTY::WRITE
);Now that our characteristic is built and has the right permissions, we give it some starting data using the setValue() method. Here, we are just loading in the simple text message "Hello World!". So when a phone connects and reads this characteristic, it will receive Hello World!.
Of course, you can replace this with something more useful, such as a sensor value, a button state, an LED state, or any other information you want to send through BLE.
pCharacteristic->setValue("Hello World!");Then we start the service using pService->start();. This step completes the service setup. If you skip this step, your service and its characteristics will remain completely hidden and inaccessible to the outside world.
pService->start();After setting up the service and characteristic, we move on to advertising. Advertising is how the ESP32 announces itself to nearby BLE devices.
We get the advertising object using NimBLEDevice::getAdvertising();. This gives us access to the advertising settings, so we can decide what information the ESP32 will share while it is waiting for a device to connect.
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();Next, we set the advertised name using pAdvertising->setName("MyESP32");. This helps a BLE scanner show a friendly name instead of showing only an unknown device or a hardware address. If you want your ESP32 to appear with a different name, you can change MyESP32 to something else.
pAdvertising->setName("MyESP32");Then we add the service UUID to the advertising data using pAdvertising->addServiceUUID(SERVICE_UUID);. This lets scanning devices know that our ESP32 is offering this particular BLE service.
pAdvertising->addServiceUUID(SERVICE_UUID);After that, we enable scan response using pAdvertising->enableScanResponse(true);. BLE advertising packets are small, so sometimes the ESP32 cannot fit all the useful information into the main advertising packet. A scan response lets the ESP32 send extra information when a scanner asks for more details. This can help the device name and service information appear more reliably in scanning apps.
pAdvertising->enableScanResponse(true);Finally, we start advertising with pAdvertising->start();. At this point, the ESP32 begins broadcasting itself.
pAdvertising->start();The loop() function is almost empty in this example because NimBLE handles BLE tasks in the background. If you want to expand this project, you can use the loop() function to read sensors, check buttons, update the characteristic value, or send notifications to a connected device without interrupting the Bluetooth connection.
void loop() {
// The FreeRTOS scheduler handles BLE tasks in the background;
// the main loop can remain empty or handle other sensor logic.
delay(2000);
}Using the nRF Connect to Test
To test the BLE connection, you’ll have to pair the ESP32 with your smartphone. You’ll also need a Bluetooth debugging application installed on it. There are several options available; one of our favorites is Nordic’s nRF Connect, which is available for both iOS and Android devices. It’s a powerful tool that allows you to scan and explore your BLE devices and communicate with them.
1. Go to the Google Play Store or the Apple App Store and search for “nRF Connect for Mobile”. Install the app and open it.

2. Ensure that your phone’s Bluetooth is turned on.

3. In the app, tap on the “SCAN” button. The app will start scanning for nearby BLE devices.

4. A list of available devices with their respective signal strengths and other details will appear. Look for “MyESP32”, and click the “Connect” button next to that.

5. You’ll be sent over to the “Services” view. There, you’ll see a list of available services. Click “Unknown Service”—the UUID string should match SERVICE_UUID in your example code.

6. Tap on a service to see its associated characteristics. Next to each characteristic, there will be two icons. The down arrow allows you to read a characteristic, whereas the up arrow allows you to write to it.

7. Tap on the down arrow to read the characteristic. You’ll see the characteristic’s UUID, READ and WRITE properties, and the value “Hello World!”, exactly as specified in our code.


