A successful connection does not automatically make the data plane clear. Many GATT problems are not caused by packets being lost. They are caused by not knowing what object is being accessed in the first place: Handle, UUID, Characteristic Value, and CCCD often get mixed together in logs.
This article focuses on the BLE 4.2 and later scenarios that matter most in development, integration, and packet analysis. Advertising and connection setup, pairing and bonding, security levels, and PHY/Data Length Extension are not expanded unless they directly affect GATT behavior.
First Separate What the Data Plane Is Actually Accessing
When reading GATT/ATT, keep this split in mind:
- ATT handles how attributes are accessed by
Handle - GATT handles how those attributes are organized into
ServiceandCharacteristic
The most direct engineering picture is a numbered data table:
App
-> GATT: defines how Service / Characteristic / Descriptor are organized
-> ATT: defines how reads, writes, discovery, and notifications work by Handle
-> L2CAP: carries ATT PDUs
What is actually transmitted on the wire is an ATT PDU, not the abstract idea of a Service or Characteristic. GATT tells both sides which Handle range belongs to which Service, which Handle contains the business value, and which Descriptor holds the notification switch.
Do not confuse the common roles
Central / Peripheralare link roles: who initiates the connection and who is being connectedGATT Client / Serverare data-access roles: who sends ATT requests and who maintains the attribute table
In most products:
- Phone or gateway:
Central + GATT Client - Sensor or peripheral:
Peripheral + GATT Server
These are not rigidly tied together, but they are the most common pairing.
What a typical session looks like
After connection, the common path is usually:
- Optional
Exchange MTU - Client discovers Primary Services
- Client discovers Characteristics and Descriptors
- Client reads static data or writes configuration
- Client writes CCCD to enable Notification or Indication
- Server later pushes data or continues answering reads and writes
When a capture is hard to understand, first decide which of these six steps you are in.
What an Attribute Table Looks Like
Every Attribute has only three core elements
| Element | Length | Purpose |
|---|---|---|
Handle |
16 bit | The access entry point |
UUID |
16 bit or 128 bit | The type identifier |
Value |
0 to 512 bytes | The actual content |
The easy thing to miss is this:
Handleis the access addressUUIDis the type- The read or write operation targets the Value behind that Handle
So when a packet says Read Request(handle=0x0025), it means “access the Value of attribute 0x0025,” not “read the UUID itself.”
GATT structure is really an organization rule for Attributes
The usual GATT hierarchy is:
Profile
-> Service
-> Characteristic
-> Descriptor
But on the wire it becomes a linear Attribute Table. A notify-capable Characteristic usually occupies at least these entries:
Handle UUID Meaning
0x0001 0x2800 Primary Service Declaration
0x0002 0x2803 Characteristic Declaration
0x0003 0x2A37 Characteristic Value
0x0004 0x2902 CCCD
Two things matter:
0x2803is a declaration, not the value itself- The client reads and writes the Value Handle, not the Declaration Handle
Why a Characteristic usually needs at least two Handles
A Characteristic minimally includes:
Characteristic DeclarationCharacteristic Value
If it supports Notification or Indication, it usually also needs CCCD (0x2902). So the common pattern is:
- Declaration Handle
- Value Handle
- CCCD Handle
They sit next to each other, but they do different jobs.
Properties are not the same as permissions
Characteristic Properties describe which operations are supported:
| Bit | Property | Meaning |
|---|---|---|
| 1 | Read | Supports read |
| 2 | Write Without Response | Supports Write Command |
| 3 | Write | Supports Write Request |
| 4 | Notify | Supports Notification |
| 5 | Indicate | Supports Indication |
But actual access still depends on permissions, for example:
- Read is allowed only after encryption
- Write is allowed only after authentication
So it is not contradictory to see Read in Properties and still receive Insufficient Authentication (0x05).
Engineering conclusion
- When reading a GATT database, find the Value Handle and CCCD first
- When a Characteristic behaves strangely, separate
Properties, permissions, and CCCD - When designing custom services, keep Handle layout simple and contiguous
The Most Common Post-Connection Flow
The usual path after connection is MTU negotiation -> Service Discovery -> Characteristic Discovery -> Descriptor Discovery.
Start with MTU negotiation
The default ATT MTU is 23 bytes, which limits how much a single PDU can carry:
Read Responsecan carry at most22 bytesof ValueWrite Requestcan carry at most20 bytes, becauseOpcodeandHandlealso consume space
Typical negotiation:
Client -> Exchange MTU Request (247)
Server -> Exchange MTU Response (185)
Final ATT MTU = min(247, 185) = 185
What matters most is:
- The negotiated result is the minimum of the two sides
- MTU is usually negotiated once per connection
- All later ATT PDUs are constrained by this MTU
Service Discovery: find the boundaries first
The client usually sends:
Read By Group Type RequestType = 0x2800for Primary Service
The important results are:
- Start Handle of the Service
- End Handle of the Service
- Service UUID
The real value of Service Discovery is not “reading the service name.” It is cutting the attribute table into service-sized segments.
Characteristic Discovery: find the value Handle
Once the service range is known, the client sends:
Read By Type RequestType = 0x2803for Characteristic Declaration
The important result is not the declaration Handle itself, but:
PropertiesValue HandleCharacteristic UUID
A very common mistake is to treat the 0x2803 Handle as the business-data Handle. In most cases, the actual data access target is the returned Value Handle.
Descriptor Discovery: mostly to find CCCD
If a Characteristic supports Notify or Indicate, the next common step is:
Find Information Request
The goal is usually to find 0x2902, the CCCD.
Quick capture checklist
- Did
Exchange MTUhappen? - Did
Read By Group Typefind the target Service? - Did
Read By Typefind the target Characteristic Value Handle? - Did
Find Informationfind0x2902? - Are later reads, writes, or notifications using the correct Handle?
If step 2 already fails to find the target Service, all later read/write failures are only symptoms, not the root cause.
Why Service Changed matters
If a device updates firmware and the GATT database changes while the client still uses an old cache, it is easy to end up with:
- Reading the wrong Handle
- Writing to an old Handle
- Writing CCCD successfully but seeing no effect
At that point, pay attention to Generic Attribute Service (0x1801) and its Service Changed (0x2A05). It tells a client that caches the database: “the Handle layout changed, you need to rediscover it.” This kind of message is usually sent through Indication, because it should not be dropped silently.
Engineering conclusion
- The discovery flow is really about building a mapping from UUID to Handle
- Service Discovery finds boundaries, Characteristic Discovery finds the
Value Handle, and Descriptor Discovery mostly looks forCCCD - If the firmware upgrade changes the GATT table structure, handle
Service Changedor an equivalent versioning strategy seriously
Reads, Writes, and Long Data Transfer
The most common read operation: Read Request
The most basic read is:
Client -> Read Request(handle)
Server -> Read Response(value)
It is suitable for:
- Device name
- Firmware version
- Current configuration value
- Low-frequency state data
If the Value length does not exceed MTU - 1, this is the most direct read method.
When the Value is too long, use Read Blob
When a single Read Response cannot carry the full data, the client keeps sending:
Read Blob Request(handle, offset)
Then it reads the data piece by piece using offsets.
The most common mistakes here are:
- Assuming “the characteristic value can be up to 512 bytes, so one Read can fetch it all,” when it still depends on MTU
- Hitting offset overflow, which commonly returns
Invalid Offset (0x07)orAttribute Not Long (0x0B)
How to choose between Write Request and Write Command
The difference is not just “one has a response and one does not.” The engineering meaning matters too.
| Operation | Characteristic | Suitable for |
|---|---|---|
Write Request |
Has a response, more reliable | Configuration parameters, control commands, important one-shot writes |
Write Command |
No response, higher link utilization | High-frequency streaming data, cases where occasional loss is acceptable |
If you are writing:
- Mode switches
- On/off control
- Calibration parameters
- Persistent configuration
then Write Request is the default first choice. Only use Write Command after you confirm the business logic can tolerate loss, retransmission, and no confirmation.
Long writes and reliable writes: Prepare / Execute Write
When the data is too large for a single packet, or you need “either everything succeeds or nothing takes effect,” use:
Prepare Write RequestExecute Write Request
This path is not common in normal sensor data streams, but it appears in:
- Large configuration writes
- Some custom protocol packaging for firmware fragments
- Multi-field updates that need atomic commit
Unless you really need atomic commit or segmented caching, do not start with Prepare/Execute Write. The implementation, buffering, and error handling are all more complicated.
Signed write is rarely the main path
Signed Write Command depends on CSRK and is not commonly the main solution in modern products. In most projects, a more common and easier-to-maintain approach is:
- Establish an encrypted connection directly
- Do ordinary read/write after the security level is sufficient
If there is no legacy compatibility constraint, focus on encrypted connections instead of Signed Write.
Engineering conclusion
- Use
Readfirst for static data and low-frequency state - Use
Write Requestfirst for important writes - Use
Notificationfor high-frequency upstream data instead of polling with frequentRead - For long-data scenarios, first calculate MTU, fragmentation, buffer, and timeout before deciding whether
Read BloborPrepare/Execute Writeis necessary
Notification, Indication, and CCCD
The essential difference between Notification and Indication
Both are server-initiated pushes to the client, but the reliability model is different.
| Mechanism | Confirmation needed | Throughput | Suitable for |
|---|---|---|---|
Notification |
No | Higher | High-frequency sensor data, continuous status reporting |
Indication |
Yes, Handle Value Confirmation |
Lower | Critical events, information that must not be dropped silently |
From a capture perspective:
- After a Notification is sent, the server does not wait for an application-layer confirmation
- After an Indication is sent, the next similar send usually waits for a
Confirmationfrom the client
So if the reporting rate is visibly blocked, suspect whether Indication was used by mistake.
CCCD is the client-side subscription switch
Support for Notify/Indicate alone is not enough. The client must also write CCCD (0x2902) to truly subscribe.
Common values:
| Value | Meaning |
|---|---|
0x0000 |
Everything off |
0x0001 |
Enable Notification |
0x0002 |
Enable Indication |
0x0003 |
Enable both |
This is also a high-frequency checkpoint in packet analysis:
- If the server never pushes data, first check whether the client successfully wrote CCCD
- If CCCD was written but there is still no data, then check whether the correct CCCD Handle was written
Why many “notification failures” are not actually Notification problems
Common root causes are usually:
- The wrong Handle was written, and the Value Handle was mistaken for the CCCD Handle
- The Characteristic supports only
Notify, but the client wrote0x0002 - Permissions were missing, and CCCD write was rejected
- CCCD was not restored after reconnect, but the application assumed it was still enabled
- The firmware upgraded and Handle values changed, but the client still used an old cache
Whether CCCD persists depends on your bonding strategy
If the device supports Bonding, it usually persists configuration associated with the peer, such as:
- Bond information
- Identity information
- CCCD configuration
- Whether Service Changed must be sent
If there is no Bonding, do not assume CCCD survives a disconnect and reconnect. The safest engineering approach is:
- After every connection, have the client explicitly check or reconfigure subscription state
Engineering conclusion
- Use Notification by default for high-frequency data; only consider Indication for critical state changes
- When Notification does not work, check CCCD first, then the Handle, then permissions
- As soon as reconnect, bonding, or upgrade is involved, consider CCCD restoration and cache invalidation together
The Most Common Development Errors and Where to Start Debugging
ATT Error Response is useful not just because it says “failed,” but because it tells you where to start.
High-frequency error codes
| Error code | Name | Priority for investigation |
|---|---|---|
0x01 |
Invalid Handle | Handle wrong, cache stale, database changed |
0x02 |
Read Not Permitted | The attribute does not support read, or you are reading the wrong object |
0x03 |
Write Not Permitted | The attribute does not support write, or you wrote to the wrong Handle |
0x05 |
Insufficient Authentication | Authentication is required, usually pairing or bonding is needed |
0x07 |
Invalid Offset | Blob offset error |
0x0A |
Attribute Not Found | Target UUID is not in the current search range |
0x0D |
Invalid Attribute Value Length | Write length does not match the requirement |
0x0F |
Insufficient Encryption | Encrypted link required |
0x11 |
Insufficient Resources | Not enough device-side buffer, queue, or memory |
How to judge first when an error appears
Use this order:
- Is the problem a wrong Handle or a permission problem?
- Is the protocol unsupported, or is the current security level insufficient?
- Is it a length/offset problem, or is the expected MTU wrong?
- Is the client cache wrong, or did the server database really change?
In particular, 0x05 and 0x0F are usually not GATT logic bugs. They mean the security preconditions were not met.
The fields most worth checking in a capture
OpcodeHandleValue HandleUUIDError CodeMTUCCCD Value
Many development problems do not require starting from the specification. Connect these fields first, and the root cause often becomes much smaller.
Engineering conclusion
Invalid Handleusually means cache and table-structure changes should be suspected firstNot Permittedusually means the wrong Handle or operation type was usedAuthentication / Encryptionerrors should take you back to pairing and security level, not just GATT code
Custom Service Design Recommendations
- When a standard Service or Characteristic can be reused, prefer the SIG-assigned UUID
- When custom UUIDs are required, keep them in one stable 128-bit UUID plan; do not generate them randomly and separately
- Do not let the UUID semantics of one Service or Characteristic change across firmware versions; add a new entry when new capability is needed
- Separate “read configuration,” “write configuration,” “status reporting,” and “large data stream” into different Characteristics instead of letting one Value do everything
- If data needs continuous reporting, design it as a Notification path first, rather than forcing the client to poll with frequent reads
- If firmware may be upgraded and the table structure may change, design cache invalidation and
Service Changedbehavior in advance
When MTU, DLE, throughput, and connection-parameter trade-offs are involved, go back to BLE Link Layer / PHY and BLE GAP Protocol Analysis.
References
Bluetooth Core Specification
Bluetooth Core SpecificationVolume 3, Part F: Attribute Protocol (ATT)Section 3: Protocol requirementsSection 4: Protocol data units
Volume 3, Part G: Generic Attribute Profile (GATT)Section 3: Service interoperability requirementsSection 4: Profile hierarchy and data relationshipsSection 5: Sub-procedures and feature requirements
- Official entry: https://www.bluetooth.com/specifications/specs/
Assigned Numbers
GATT Services: https://www.bluetooth.com/specifications/assigned-numbers/GATT Characteristics: https://www.bluetooth.com/specifications/assigned-numbers/GATT Descriptors: https://www.bluetooth.com/specifications/assigned-numbers/
Further Reading
- BLE Architecture Overview: go back to the layering boundary and unified debugging entry point
- BLE Advertising Analysis: go back to the advertising entry point, device visibility, and pre-connection filtering
- BLE Link Layer / PHY: go back to connection events, retransmission, and link-layer transport behavior
- BLE GAP Protocol Analysis: go back to connection parameters, discovery entry points, and how security levels affect the data plane experience
- BLE SMP Secure Pairing: continue with how authentication, encryption, and bonding change attribute-access results