Descriptor is an auxiliary attribute of a BLE characteristic that provides metadata or configuration for its value. The Bluetooth Core Specification 5.4 defines descriptors as an integral part of the GATT profile: they describe units of measurement, value ranges, text names, and manage subscription to notifications. The most well-known descriptor is the Client Characteristic Configuration Descriptor (CCCD, 0x2902), which allows the Central to subscribe to notifications from the characteristic. According to Bluetooth SIG, 8 standard descriptors are specified, and custom descriptors can be created by the developer for any additional metadata.
Key Takeaways
Descriptor is a GATT protocol attribute that attaches to a Characteristic and provides additional information about its value or how to use it. In GATT terminology, the characteristic is the data, and the descriptor is the metadata of that data.
Each Characteristic can have zero or more descriptors. Descriptors are defined as separate attributes in the GATT table and have their own 16-bit UUIDs in the Bluetooth SIG space. Descriptors cannot exist independently — they are always tied to a specific characteristic.
According to the Bluetooth Core Specification 5.4 (2023), descriptors are divided into two categories: standard (defined by Bluetooth SIG) and custom (defined by the developer). Standard descriptors have UUIDs in the range 0x2900–0x2908 and perform strictly defined functions. Custom descriptors use 128-bit UUIDs, similar to custom services and characteristics.
Descriptors are a key mechanism for configuring the behavior of a BLE connection. Without CCCD (0x2902) it is impossible to subscribe to notifications, without Presentation Format (0x2904) the Central will not know how to interpret the raw bytes of the characteristic.
CCCD with UUID 0x2902 is the most important and widely used descriptor in BLE. It manages the Central's subscription to Notify and Indicate from the characteristic. Without writing to CCCD, the characteristic will not send notifications, even if it has the Notify property.
CCCD is a 16-bit value where the least significant bit (0x0001) enables Notify, and the second bit (0x0002) enables Indicate. The Central writes the desired value to CCCD, thereby subscribing to notifications. If the Central writes 0x0000 — the subscription is cancelled.
The CCCD mechanism works as follows: when the Central discovers a characteristic that supports Notify, it finds its CCCD descriptor and writes 0x0001. After that, every time the Peripheral updates the characteristic value, it is automatically sent to the subscribed Central via Handle Value Notification (without a request from the Central).
According to the Apple Core Bluetooth Programming Guide, CCCD is automatically managed by the system when setNotifyValue is called for a characteristic. The developer does not need to manually find and write CCCD — Core Bluetooth does this automatically when notifications are enabled.
In iOS, subscription is done via the setNotifyValue method on the CBPeripheral object.
import CoreBluetooth
// Subscribe to characteristic notifications
peripheral.setNotifyValue(true, for: characteristic)
// Delegate receives updated values
func peripheral(
_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?
) {
// characteristic.value has new data
guard let data = characteristic.value else { return }
}
On Android, subscription to notifications is done via BluetoothGatt.setCharacteristicNotification. The developer must also manually write the value 0x0001 to CCCD.
import android.bluetooth.*;
// Enable notifications
bluetoothGatt.setCharacteristicNotification(characteristic, true);
// Write 0x0001 to CCCD
BluetoothGattDescriptor descriptor = characteristic
.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
Bluetooth SIG has defined 8 standard descriptors for common characteristic configuration tasks. Each descriptor has a strictly defined UUID and value format.
| UUID | Descriptor Name | Purpose | Value Type |
|---|---|---|---|
| 0x2900 | Characteristic Extended Properties | Additional characteristic properties | uint16 (bit mask) |
| 0x2901 | Characteristic User Description | Text name of the characteristic | UTF-8 string |
| 0x2902 | Client Characteristic Configuration | Notification management | uint16 |
| 0x2903 | Server Characteristic Configuration | Server configuration for broadcast | uint16 |
| 0x2904 | Characteristic Presentation Format | Data presentation format | Structure (7 bytes) |
| 0x2905 | Characteristic Aggregate Format | Combining multiple characteristics | List of UUIDs |
| 0x2906 | Valid Range | Range of valid values | uint16 × 2 (min, max) |
| 0x2907 | External Report Reference | External report reference | uint16 |
| 0x2908 | Report Reference | Internal report reference | uint8 + uint16 |
Characteristic User Description (0x2901) is especially useful for UI: it contains a human-readable name of the characteristic that can be displayed in the application interface. For example, a Battery Level characteristic may have a User Description descriptor with the value “Battery charge level”.
Characteristic Presentation Format (0x2904) contains information on how to interpret the characteristic value: format (uint8, uint16, float), exponent, unit of measurement (via UUID from the Bluetooth SIG registry), namespace, and description. For example, temperature can be int16 with exponent -2 (division value 0.01°C) and unit of measurement “degree Celsius”.
Access to descriptors differs on iOS and Android. Core Bluetooth automatically manages CCCD when notifications are enabled, while Android requires manual writing of the value to the descriptor. Let's look at both approaches.
In iOS, the developer usually does not interact with descriptors directly. Core Bluetooth automatically finds CCCD and writes the required value when setNotifyValue is called. The discoverDescriptors method is used to read descriptors.
// Discover descriptors
peripheral.discoverDescriptors(for: characteristic)
// Read User Description value
func peripheral(
_ peripheral: CBPeripheral,
didDiscoverDescriptorsFor characteristic: CBCharacteristic,
error: Error?
) {
for descriptor in characteristic.descriptors ?? [] {
if descriptor.uuid == CBUUID("2901") {
peripheral.readValue(for: descriptor)
}
}
}
On Android, the developer must find the CCCD descriptor of the characteristic and write the value to enable notifications. This is an additional step compared to iOS.
// Read User Description descriptor value
BluetoothGattDescriptor userDesc = characteristic
.getDescriptor(
UUID.fromString("00002901-0000-1000-8000-00805F9B34FB")
);
if (userDesc != null) {
bluetoothGatt.readDescriptor(userDesc);
}
Custom descriptors are used when the standard Bluetooth SIG descriptors are not sufficient. For example, a medical sensor manufacturer might add a descriptor with sensor calibration information or a descriptor with an error log.
A custom descriptor is created with a 128-bit UUID, similar to a custom characteristic. On the Peripheral side, the developer adds it to the characteristic via the appropriate API. On the Central side, the descriptor is discovered automatically when discoverDescriptors is called.
When creating a custom descriptor, follow these rules: the value length must match the expected data type, and access rights (read/write) must be explicitly specified. Do not use UUIDs from the standard Bluetooth SIG descriptor range (0x2900–0x2908) for custom purposes.
Characteristic and descriptor are different levels of GATT attributes. A characteristic is a data point, while a descriptor is its metadata or configuration. The difference manifests in several aspects: purpose, UUID, and access rules.
A characteristic has a mandatory declaration that is always present in the GATT table. A descriptor is optional — a characteristic may have no descriptors. A characteristic can be read, written, or send notifications independently. A descriptor is always tied to a characteristic and does not exist separately.
According to the Bluetooth Core Specification 5.4, the order of attributes in the GATT table is as follows: service declaration, characteristic declaration, characteristic value, characteristic descriptors (if any), then the next service. This order is mandatory for correct GATT client operation.
Frequently Asked Questions
CCCD is needed for subscription management: the Notify property only indicates that the characteristic can send notifications, but does not enable them. The Central must explicitly subscribe by writing 0x0001 to CCCD. This prevents unwanted data transmission to an unprepared Central.
The specification does not limit the number of descriptors. In practice, each characteristic usually has 1–3 descriptors: CCCD (for characteristics with Notify/Indicate), optionally User Description and Presentation Format. Too many descriptors increase the GATT table size.
In most cases, no. Core Bluetooth on iOS and Android automatically add CCCD to characteristics that have Notify or Indicate properties. Custom descriptors are created manually only for specific metadata.
No, CCCD has a strictly defined format (uint16) and purpose — notification management. Writing other values violates the Bluetooth specification and may cause the subscription to stop working. For custom data, use custom descriptors with 128-bit UUIDs.
The User Description contains a human-readable name of the characteristic — for example, “Case temperature” instead of UUID 0x2A6E. The application can display this name in the interface, making interaction with the BLE device understandable for the user without hardcoded strings.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also