Bluetooth SDP Explained: How Devices Discover Each Other’s Services
Service Discovery Protocol · Service Records · UUID · BlueZ sdptool
When two Bluetooth devices meet for the first time, how does your phone know the speaker supports A2DP audio, or the keyboard supports HID? The answer is SDP — Service Discovery Protocol. Before any data flows, SDP runs in the background and answers one simple question: “What services do you support?”
This post breaks down how SDP works, what a service record looks like, how UUIDs identify services, and how to inspect SDP records on a real Linux machine using BlueZ’s sdptool.
Keywords covered in this post:
SDP Client SDP Server Service Record Service Attribute Attribute ID Attribute Value Service Class UUID 128-bit UUID 16-bit alias sdptool BlueZ SDP
1. What Is SDP and Why Does It Exist?
When you connect a Bluetooth device, the stack needs to answer two questions in the right order:
- What services does the remote device offer? — This is SDP’s job.
- How do I use those services? — Done by the specific profile (A2DP, HFP, HID…) after SDP finishes.
SDP only answers question 1. Think of it as the directory board in a building — it tells you which office is on which floor, but doesn’t take you there.
Key point: SDP discovers what services are available. Actual data transfer (audio, serial data, HID reports) happens through profiles like A2DP, RFCOMM, HID — after SDP finishes its job.
2. SDP Client–Server Model
SDP uses a client–server architecture. Every device that wants to offer services runs an SDP server. A device that wants to discover those services acts as an SDP client.
Important rule: Only one SDP server is allowed per Bluetooth device. If a device only consumes services and never provides them, it can skip the SDP server entirely and run as a client-only device.
Figure 1 — SDP Client–Server Architecture
SDP Client
(e.g. smartphone)
ServiceSearchRequest
ServiceAttrRequest
ServiceSearchAttrReq
SDP Server
(maintains all service records)
Service Record 1
(e.g. A2DP Audio Sink)
0x0001 · ServiceClassIDList
0x0004 · ProtocolDescList
…
Service Record 2
(e.g. HFP Hands-Free)
0x0001 · ServiceClassIDList
0x0004 · ProtocolDescList
…
… more records
(SPP, HID, OBEX…)
One SDP server per Bluetooth device · All records stored in server · Client queries on demand
The SDP server listens on a fixed L2CAP channel (PSM = 0x0001). The three main request types are:
ServiceSearchRequest
Find all service records matching a UUID. Returns a list of ServiceRecord Handles.
ServiceAttributeRequest
Get specific attributes from a record you already have the handle for.
ServiceSearchAttributeRequest
Search by UUID and return attributes in one shot. The most commonly used request.
3. Service Records and Service Attributes
Every service a Bluetooth device offers is described by a Service Record stored inside the SDP server. A record is simply a list of Service Attributes. Each attribute is a key–value pair:
- Attribute ID — a 16-bit unsigned integer that uniquely names the attribute within a record.
- Attribute Value — a variable-length field whose meaning depends on the Attribute ID and the service class the record belongs to.
Figure 2 — Inside a Service Record (A2DP Audio Sink Example)
Service Record — A2DP Audio Sink | Handle: 0x00010001
| Attribute ID (16-bit) |
Attribute Name |
Attribute Value (variable length) |
| 0x0000 |
ServiceRecordHandle |
0x00010001 (32-bit handle, unique per server) |
| 0x0001 |
ServiceClassIDList |
{ UUID 0x110B (AudioSink), UUID 0x1203 (GenericAudio) } |
| 0x0004 |
ProtocolDescriptorList |
{ L2CAP(PSM=0x0019), AVDTP(ver=0x0103) } |
| 0x0005 |
BrowseGroupList |
{ UUID 0x1002 (PublicBrowseRoot) } |
| 0x0100 |
ServiceName |
“Audio Sink” (UTF-8 string) |
| 0x0311 |
SupportedFeatures |
0x0001 (Headphone category bitmask) |
Standard Attribute IDs 0x0000–0x01FF are defined by the Bluetooth SIG · Profile-specific IDs start from 0x0200
Data Element encoding: Inside an SDP PDU, attribute values are encoded in a compact binary format called Data Element — a type-descriptor byte followed by the payload. Types include: nil, unsigned int, signed int, UUID, text string, boolean, sequence, and URL.
4. Service Class
Every service record belongs to a Service Class. The service class definition specifies:
- Which attributes are mandatory for records of that type.
- Which attributes are optional.
- What data types and allowed values each attribute may hold.
For example, the AudioSink service class (part of A2DP) mandates that a record includes ServiceClassIDList, ProtocolDescriptorList, and BrowseGroupList. Without these, an SDP client cannot figure out how to connect.
Key fact: Each service class is assigned a UUID. This UUID is what the SDP client searches for when it wants to find a specific type of service on a remote device.
5. UUID Deep Dive — 128-bit, 16-bit, 32-bit
A UUID (Universally Unique Identifier) is a 128-bit number guaranteed to be globally unique — without any central authority. Every service class, protocol, and profile has its own UUID. The Bluetooth Assigned Numbers document maintains the table of pre-assigned UUIDs for all standard services.
Figure 3 — UUID Formats in Bluetooth SDP
Full 128-bit UUID — always valid, used for custom/vendor profiles
XXXXXXXX – XXXX – XXXX – XXXX – XXXXXXXXXXXX
32 hex digits · 16 bytes · RFC 4122 format
Bluetooth Base UUID — used as the expansion base for all standard service UUIDs
00000000-0000-1000-8000-00805F9B34FB
Defined by the Bluetooth SIG · Bluetooth Core Spec Vol 3 Part B §2.5.1
▼ 16-bit or 32-bit alias expands into the base UUID by substituting into positions 0–3 ▼
16-bit UUID alias
0x110B → AudioSink
Expands to full 128-bit UUID:
0000110B-0000-1000-8000-00805F9B34FB
Formula: 0000XXXX + base UUID suffix
32-bit UUID alias
0x0000110B → AudioSink
Same expansion, uses 32-bit field:
0000110B-0000-1000-8000-00805F9B34FB
Useful when 16-bit space is exhausted
Commonly Used 16-bit Service Class UUIDs (Bluetooth Assigned Numbers)
0x1101 = SPP 0x110A = A2DP Source 0x110B = A2DP Sink 0x111E = HFP 0x1124 = HID 0x1002 = PublicBrowseRoot 0x1200 = DeviceID
The Bluetooth Base UUID is a fixed value defined in the spec. Below is how it looks in BlueZ source code alongside the standard service class UUID macros:
/* Bluetooth Base UUID — Bluetooth Core Spec Vol 3, Part B, Section 2.5.1 */ /* Any 16-bit alias X expands to: 0000XXXX-0000-1000-8000-00805F9B34FB */ #define BT_BASE_UUID \ “00000000-0000-1000-8000-00805F9B34FB” /* Common 16-bit Service Class UUIDs — from BlueZ lib/uuid.h */ #define AUDIO_SOURCE_SVCLASS_ID 0x110A /* A2DP Source */ #define AUDIO_SINK_SVCLASS_ID 0x110B /* A2DP Sink */ #define HANDSFREE_SVCLASS_ID 0x111E /* HFP Hands-Free */ #define SERIAL_PORT_SVCLASS_ID 0x1101 /* SPP Serial Port */ #define OBEX_PUSH_SVCLASS_ID 0x1105 /* OBEX Object Push */ #define HUMAN_INTERFACE_SVCLASS_ID 0x1124 /* HID */ #define PNP_INFO_SVCLASS_ID 0x1200 /* Device Identification */
BlueZ ships with sdptool — a command-line tool to query the SDP server of any Bluetooth device, local or remote. It is the go-to tool when debugging why a Bluetooth profile is not connecting.
6.1 Browse Your Own Device’s SDP Records
# List all service records registered on the local Bluetooth adapter $ sdptool browse local # Typical output (abbreviated): Service Name: Audio Sink Service RecHandle: 0x10001 Service Class ID List: “Audio Sink” (0x110b) Protocol Descriptor List: “L2CAP” (0x0100) PSM: 25 “AVDTP” (0x0019) uint16: 0x0103 Service Name: Headset Audio Gateway Service RecHandle: 0x10002 Service Class ID List: “Headset Audio Gateway” (0x1112) Protocol Descriptor List: “L2CAP” (0x0100) “RFCOMM” (0x0003) Channel: 13
6.2 Query a Remote Device
# Browse all services on a remote Bluetooth device (replace MAC) $ sdptool browse XX:XX:XX:XX:XX:XX # Search for a specific service UUID on a remote device $ sdptool search –bdaddr XX:XX:XX:XX:XX:XX 0x110b # A2DP Sink $ sdptool search –bdaddr XX:XX:XX:XX:XX:XX 0x111e # HFP Hands-Free $ sdptool search –bdaddr XX:XX:XX:XX:XX:XX 0x1101 # SPP Serial Port # Get all records with full raw attribute details $ sdptool records XX:XX:XX:XX:XX:XX
6.3 Manually Register an SDP Record (Testing)
# Register a Serial Port Profile (SPP) service record on RFCOMM channel 22 $ sdptool add –channel=22 SP # Register an OBEX Push service record on channel 9 $ sdptool add –channel=9 OPUSH # Delete a record by its handle $ sdptool del 0x10005 # Verify — check what is now registered locally $ sdptool browse local | grep -A5 “Serial Port”
BlueZ 5.x note: In BlueZ 5 and above, sdptool runs in compatibility mode against the legacy SDP daemon. For production code, SDP records are registered through the bluetoothd D-Bus profile registration API, not via sdptool add directly.
7. Browsing SDP Records from Python
You can query SDP records programmatically using Python’s bluetooth module (PyBluez) or via BlueZ D-Bus. The example below finds the RFCOMM channel for an SPP service — exactly what happens internally when you connect to a serial Bluetooth device.
#!/usr/bin/env python3 “”” Find RFCOMM channel for Serial Port Profile (SPP) via SDP query. Requires: Linux + BlueZ + PyBluez (pip install pybluez) “”” import bluetooth TARGET_ADDR = “XX:XX:XX:XX:XX:XX” # replace with actual MAC def find_spp_channel(addr): # SPP UUID — full 128-bit form required by find_service() SPP_UUID = “00001101-0000-1000-8000-00805f9b34fb” # Internally sends a ServiceSearchAttributeRequest over L2CAP PSM 1 services = bluetooth.find_service(uuid=SPP_UUID, address=addr) if not services: print(“No SPP service found on this device”) return None for svc in services: print(f”Service : {svc[‘name’]}”) print(f”Protocol : {svc[‘protocol’]}”) print(f”Channel : {svc[‘port’]}”) # RFCOMM channel from ProtocolDescList print(f”Handle : {svc[‘handle’]}”) # ServiceRecordHandle print() return services[0][‘port’] if __name__ == “__main__”: ch = find_spp_channel(TARGET_ADDR) if ch: print(f”Connect via RFCOMM channel {ch}”)
7.1 Reading Cached SDP Results via BlueZ D-Bus
#!/usr/bin/env python3 “”” Read discovered service UUIDs from bluetoothd’s cache via D-Bus. bluetoothd caches SDP results automatically after pairing/connecting. “”” import dbus bus = dbus.SystemBus() DEVICE_PATH = “/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX” device_obj = bus.get_object(“org.bluez”, DEVICE_PATH) device_props = dbus.Interface(device_obj, “org.freedesktop.DBus.Properties”) # UUIDs property = list of service class UUIDs found during SDP props = device_props.GetAll(“org.bluez.Device1”) uuids = props.get(“UUIDs”, []) print(“Services discovered via SDP:”) for u in uuids: print(f” {u}”) # Example output: # 00001101-0000-1000-8000-00805f9b34fb → SPP # 0000110b-0000-1000-8000-00805f9b34fb → A2DP Sink # 0000111e-0000-1000-8000-00805f9b34fb → HFP Hands-Free
What happens under the hood: bluetooth.find_service() sends a ServiceSearchAttributeRequest PDU over L2CAP PSM 1 to the remote SDP server. The response is a serialized list of service records in Data Element binary format. PyBluez decodes this and returns a plain Python dict per record.
How It All Comes Together
Figure 4 — Full SDP Flow During Bluetooth Connection Setup
1
Inquiry / Scan
Phone (SDP client) discovers headphone (SDP server) via BR/EDR inquiry or BLE scan. Gets device MAC address.
↓
2
L2CAP Connect → PSM 0x0001
Phone opens an L2CAP channel to the headphone. PSM 0x0001 is the fixed, reserved channel for SDP — no discovery needed for this step.
↓
3
ServiceSearchAttributeRequest
Phone asks: “Give me all records matching UUID 0x110B (AudioSink), and return all their attributes in one shot.”
PDU → L2CAP PSM 1 → SDP server on headphone
↓
4
ServiceSearchAttributeResponse
Headphone SDP server replies with a service record. Phone parses it and learns: L2CAP PSM = 0x0019, AVDTP version 1.3.
Response ← serialized Data Element list of attributes
↓
5
SDP Disconnects · Profile Connects
L2CAP PSM 1 closes. Phone now opens L2CAP PSM 0x0019 and runs AVDTP/A2DP for actual audio streaming. SDP is done — it is never used again during this session.
SDP runs only during connection setup · It closes before any profile data begins to flow
8. Summary
What SDP Does
- Discovers which services a device offers
- Returns service records on request
- Identifies services by UUID
- Tells the client which PSM or RFCOMM channel to use
What SDP Does NOT Do
- Does not transfer audio, data, or commands
- Does not stay connected during data transfer
- Does not enforce security (that’s SMP + link encryption)
- Does not apply to BLE (BLE uses GATT service discovery)
Quick Reference — Key SDP Facts
L2CAP PSM = 0x0001 One SDP server per device UUID = 128-bit identifier 16-bit alias + Base UUID Service Record = list of Attributes Attribute ID = 16-bit unsigned int sdptool browse local ServiceSearchAttributeRequest SDP-only for Classic BT BLE uses GATT instead
Continue Learning Bluetooth
Next up: RFCOMM — the serial port emulation layer that sits on top of L2CAP and gives applications a familiar socket-like interface for Classic Bluetooth data transfer.
Browse All Posts Bluetooth Series