GATT (Generic Attribute Profile) is a Bluetooth Low Energy (BLE) profile that defines the data structure and rules for information exchange between BLE devices. GATT is built on top of the Attribute Protocol (ATT) and organizes data into a hierarchy: services, characteristics, and descriptors. According to Bluetooth SIG (2025), the GATT profile is used in 98% of all BLE applications — from fitness trackers to smart locks and medical sensors.
Key Takeaways
GATT (Generic Attribute Profile) is a Bluetooth Low Energy (BLE) profile that defines how two BLE devices exchange data through the Attribute Protocol (ATT). GATT standardizes the data structure: all data is organized in a “service → characteristic → descriptor” hierarchy. The profile was introduced in the Bluetooth 4.0 specification (2010) alongside BLE and has remained the primary data transfer mechanism for energy-efficient Bluetooth devices ever since. Unlike classic Bluetooth, where data is transmitted through a serial port (SPP), GATT provides structured access to data through read, write, and notification operations.
The BLE stack consists of several layers: Physical Layer (radio), Link Layer (connection management), L2CAP (logical link control), ATT (Attribute Protocol — attribute access), and GATT (profile based on ATT). GATT is the topmost layer that application developers work with. The underlying ATT provides basic operations: read, write, notify, and indicate attributes. GATT adds semantics: it defines what a service and characteristic are, how they are grouped, and what rules apply when reading and writing them. According to the Bluetooth Core Specification 5.4 (2023), GATT supports up to 65,535 attributes (services + characteristics + descriptors) on a single device.
The GATT hierarchy consists of three levels. A Service is a logical group of characteristics that solves one task (e.g., “Battery Service” or “Heart Rate Service”). A Characteristic is a data unit with a known type: current battery level, sensor reading, switch state. Each characteristic has a value and one or more Descriptors that describe metadata: measurement units, notification settings, value range. UUID (Universally Unique Identifier) uniquely identifies each service and each characteristic.
GATT supports four types of operations for interacting with characteristics. Read — the client requests the current value of a characteristic from the server. Write — the client sends a new value to the server. Notify — the server sends a value to the client without acknowledgment (faster but less reliable). Indicate — the server sends a value with acknowledgment (more reliable but slower). The developer chooses the type based on the scenario: for heart rate sensor readings, Notify is sufficient; for writing smart lock configuration, Write with acknowledgment is needed.
// GATT data structure in Android code
data class BleService(
val uuid: UUID,
val characteristics: List<BleCharacteristic>
)
data class BleCharacteristic(
val uuid: UUID,
val properties: Int, // READ, WRITE, NOTIFY, INDICATE
val descriptors: List<BleDescriptor>,
var value: ByteArray?
)
The BLE specification defines two main profiles: GAP (Generic Access Profile) and GATT (Generic Attribute Profile). GAP is responsible for device discovery, connection establishment, and visibility management — it is the “network layer” of BLE. GATT is responsible for data exchange after connection is established — the “application layer.” The developer uses GAP for scanning and connecting to a device, and GATT for reading, writing, and receiving notifications from the connected device.
| Characteristic | GAP | GATT |
|---|---|---|
| Purpose | Discovery and connection | Data exchange |
| Roles | Central / Peripheral | Client / Server |
| Protocol | HCI, Link Layer | ATT (Attribute Protocol) |
| Phase | Before connection | After connection |
| Main class | BluetoothAdapter | BluetoothGatt |
GATT defines two roles: GATT Server and GATT Client. The Server is a device that provides data (e.g., a fitness tracker sending heart rate readings). The Client is a device that requests data (e.g., a smartphone reading the readings). In most scenarios, the Android app acts as the GATT Client, and the BLE peripheral acts as the GATT Server. However, Android can also be a GATT Server — for example, when an app emulates a BLE device for other devices. The role is determined at the GATT connection setup stage and does not change during the session.
The GATT communication process begins after the BLE connection between devices is established. First, the GATT Client discovers services on the GATT Server through Service Discovery — Android performs this automatically upon connection via BluetoothGatt.discoverServices(). After discovery, the client gets a list of available services, characteristics, and descriptors. The client can then read characteristic values (Read), write new values (Write), or subscribe to notifications (Set Notify/Indicate). The GATT Server can send Notify/Indicate to the client at any time after the connection is established.
By default, the size of a single GATT packet (MTU, Maximum Transmission Unit) in BLE is 23 bytes, of which 3 bytes are the ATT header and 20 bytes are payload. If the application needs to transmit more data (e.g., OTA firmware update), MTU can be increased to 517 bytes via requestMtu() in Android. An increased MTU reduces the number of packets needed to transmit a single data block — instead of 50 small packets, you can send 2 large ones, which lowers power consumption and speeds up transmission. The maximum MTU depends on the BLE version and chip capabilities — Bluetooth 5.0 supports up to 517 bytes, older versions up to 247 bytes.
// Subscribe to characteristic notifications
fun enableNotifications(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
gatt.setCharacteristicNotification(characteristic, true)
// Enable CCCD (Client Characteristic Configuration Descriptor)
val cccdUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
val descriptor = characteristic.getDescriptor(cccdUuid)
descriptor?.let {
it.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
gatt.writeDescriptor(it)
}
}
To receive notifications from the GATT Server on Android, you need to implement the BluetoothGattCallback.onCharacteristicChanged() callback. In this method, the app receives the updated characteristic value each time the server sends a Notify or Indicate. For real-time data processing, use a buffer and Kotlin coroutines — this prevents blocking the UI thread during intensive notifications (e.g., 100+ heart rate readings per second). For Indications, you must call gatt.sendResponse() on the server side — on the Android client, this is handled automatically.
On Android, working with GATT is implemented through the classes of the android.bluetooth package: BluetoothGatt (connection), BluetoothGattService (service), BluetoothGattCharacteristic (characteristic), and BluetoothGattDescriptor (descriptor). Connecting to a BLE device starts by calling BluetoothDevice.connectGatt() — this method returns a BluetoothGatt through which all subsequent operations are performed. All GATT callbacks arrive in BluetoothGattCallback — this is an asynchronous interface that is called on the same thread where BluetoothGatt was created. Important: all GATT operations must be performed sequentially — calling multiple operations simultaneously on the same BluetoothGatt leads to errors.
Proper management of the GATT connection lifecycle is critical for BLE app stability. After calling connectGatt(), the app waits for the onConnectionStateChange() callback with STATE_CONNECTED state. Then the system automatically starts Service Discovery, after which onServicesDiscovered() is called. Only after this can you perform reading, writing, and subscription. When finished, always call gatt.close() in onDestroy() or onPause() — unclosed GATT connections drain the battery and may block reconnection to the same device on Android.
// Connect to BLE device via GATT
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
val service = gatt.getService(UUID.fromString("180D"))
val characteristic = service?.getCharacteristic(
UUID.fromString("2A37")
)
characteristic?.let { enableNotifications(gatt, it) }
}
}
Bluetooth SIG (Special Interest Group) defines dozens of standard GATT profiles and services with fixed 16-bit UUIDs. The most common ones: Battery Service (UUID 180F, Battery Level characteristic 2A19), Heart Rate service (UUID 180D, Heart Rate Measurement characteristic 2A37), Device Information service (UUID 180A, Manufacturer Name and Serial Number characteristics). Using standard services guarantees compatibility between devices from different manufacturers — any fitness bracelet with a Heart Rate service should work with any Android app that supports it.
If standard services do not cover the task, the developer can create custom GATT services with 128-bit UUIDs. When designing a custom service, you need to: define a logical group of characteristics (e.g., “Lock Control Service”), assign each characteristic the correct properties (Read, Write, Notify), define allowed values and measurement units through the Characteristic Presentation Format descriptor. For complex protocols, it is recommended to include a Command characteristic with Write property and a Status characteristic with Notify — this follows the Command/Status pattern adopted in industrial BLE applications.
// Create custom GATT service (Android as GATT Server)
private fun createCustomService(): BluetoothGattService {
val serviceUuid = UUID.fromString("12345678-1234-5678-1234-56789abcdef0")
val service = BluetoothGattService(
serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY
)
val charUuid = UUID.fromString("12345678-1234-5678-1234-56789abcdef1")
val characteristic = BluetoothGattCharacteristic(
charUuid,
BluetoothGattCharacteristic.PROPERTY_READ or
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ
)
service.addCharacteristic(characteristic)
return service
}
Frequently Asked Questions
GATT (Generic Attribute Profile) is a Bluetooth Low Energy profile that defines the data exchange structure between devices. It is needed to standardize information access: all BLE devices organize data into services and characteristics, allowing any client to read sensor readings, control devices, and receive notifications.
GAP handles BLE device discovery and connection (scanning, advertising, connection setup). GATT handles data exchange after connection (reading, writing, notifications). GAP works before connection, GATT works after. Both profiles are mandatory for BLE, but perform different functions: GAP is the “network layer,” GATT is the “application layer.”
Standard services include Battery Service (180F, battery level), Heart Rate (180D, pulse), Device Information (180A, device data). Each service contains several characteristics with 16-bit UUIDs. Developers can create custom services with 128-bit UUIDs for specific tasks — such as smart lock control or fitness tracker data transmission.
On Android, GATT is implemented through BluetoothGatt (connection), BluetoothGattService (service), BluetoothGattCharacteristic (characteristic), and BluetoothGattDescriptor (descriptor). Connection is made via connectGatt(), after which events come through the BluetoothGattCallback: onConnectionStateChange, onServicesDiscovered, onCharacteristicChanged (for notifications). All GATT operations must be performed sequentially.
UUID (Universally Unique Identifier) is a 16-bit or 128-bit identifier that uniquely identifies a service or characteristic in GATT. Standard Bluetooth SIG services use 16-bit UUIDs (e.g., 180D for Heart Rate). Custom developer services use 128-bit UUIDs (e.g., 12345678-1234-5678-1234-56789abcdef0). UUID allows the client to find the required data on the GATT Server.
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