A quick bluetoothctl show confirmed that indeed the bluetooth adapter was in discovery mode. What was surprising is that I couldn't stop the discovery with bluetoothctl scan off, because that just gave "Failed to stop discovery: org.bluez.Error.Failed".

My suspicion was that something is constantly triggering a scan, and I knew that the bluetooth service exposes a dbus interface that other applications could call remotely, so the most likely culprit was that some application uses that to constantly start discovery.

Listening for dbus messages

I have basically zero experience with dbus, all I knew about it is that it's a message bus between applications on linux, so I had to get familiar with some tools. I found dbus-monitor but my impression is that it's quite barebones compared to busctl, which comes by default with systemd, so I ended up digging into that. The manual mentions a few commands that are interesting, list, monitor, introspect, tree, status.

Let's start with busctl list to list all applications registered on dbus:

sh
$ busctl list

NAME                             PID PROCESS         USER             CONNECTION    UNIT                      SESSION DESCRIPTION
...
:1.695                         97102 bluetoothd      root             :1.695        user@1000.service         -       -
...

This means bluetoothd has the dbus name 1.695. Now, we could start monitoring messages being sent to this "name":

sh
$ busctl monitor :1.695

Monitoring bus message stream.

...
 Type=method_call  Endian=l  Flags=0  Version=1 Cookie=604  Timestamp="Thu 2026-01-01 18:16:04.745527 UTC"
  Sender=:1.872  Destination=org.bluez  Path=/org/bluez/hci0  Interface=org.bluez.Adapter1  Member=StartDiscovery
  UniqueName=:1.872
  MESSAGE "" {
  };
...

There was a lot more messages, but I'm only showing the one that is interesting. It mentions that the sender is :1.872, so back to busctl, and we can see that:

sh
$ busctl status :1.872

PID=128120
PIDFD=yes
PPID=125888
TTY=pts/8
UID=1000
EUID=1000
SUID=1000
FSUID=1000
OwnerUID=1000
GID=1000
EGID=1000
SGID=1000
FSGID=1000
SupplementaryGIDs=936 937 953 957 967 971 985 994 998 1000 1001 1001
Comm=kdeconnectd
Exe=/usr/bin/kdeconnectd

The culprit is kdeconnectd! After killing it, the scanning stops and everything is back to normal, especially my laptop's battery drain. Very strange, because I have been using it for years now, and I only noticed this problem recently.

But anyway, even though I have the culprit, I feel like I just got lucky by catching that StartDiscovery in the monitor tool, and feel unsatisfied. I have this problem on the operating table already, so this is a good opportunity to learn a bit about dbus.

What would I actually do in a situation where

  1. I don't know what I'm looking for
  2. There is so much traffic that I can't just pluck a message out from the log stream
  3. It happens so sporadically that I have no idea when it might even show up in the monitor

Let's get back to the bluetoothd service, which had a dbus name 1.695. How do we figure out what kind of functionality it even exposes on dbus, so we know what to look for? It might not even expose anything.

We can use busctl tree to see the interfaces it exposes:

busctl tree :1.695
└─ /org
  └─ /org/bluez
    └─ /org/bluez/hci0
      ├─ /org/bluez/hci0/dev_6C_93_08_63_50_A5
      ├─ /org/bluez/hci0/dev_6C_93_08_63_60_D6
      ├─ /org/bluez/hci0/dev_98_B6_EC_FE_A6_C6
      ├─ /org/bluez/hci0/dev_E4_17_D8_D6_A6_30
      ├─ /org/bluez/hci0/dev_F9_57_81_FC_84_9C
      └─ /org/bluez/hci0/dev_F9_57_81_FC_84_9E

This corresponds to my 1 bluetooth adapter (hci0), and a bunch of connected devices. We can check what the adapter exposes using introspect:

sh
$ busctl introspect :1.695 /org/bluez/hci0

NAME                                TYPE      SIGNATURE RESULT/VALUE                             FLAGS
org.bluez.Adapter1                  interface -         -                                        -
.GetDiscoveryFilters                method    -         as                                       -
.RemoveDevice                       method    o         -                                        -
.SetDiscoveryFilter                 method    a{sv}     -                                        -
.StartDiscovery                     method    -         -                                        -
.StopDiscovery                      method    -         -                                        -
.Address                            property  s         "AC:12:03:D6:D0:A2"                      emits-change
.AddressType                        property  s         "public"                                 emits-change
.Alias                              property  s         "redacted"                               emits-change writable

Oh that's great, we can see all the methods and properties it has. So if we approach the problem from this angle, we just need a way to filter down messages with monitor to the correct method call:

busctl monitor --match "destination=org.bluez.hci0,type='method_call',member='StartDiscovery'"

And this will only show messages calling the method StartDiscovery on the hci0 adapter. There is some more info on match rules in the official docs.

Trying to approach it from the bluetoothd angle

What if don't immediately zoom in on dbus, and we approach this from that angle? We would have to see some debug messages from bluetoothd. In order to do that we have to stop the bluetoothd service, then start the daemon in a foreground process with debugging enabled:

sh
$ systemctl stop bluetooth
$ /usr/lib/bluetooth/bluetoothd -n -d

bluetoothd[123202]: Bluetooth daemon 5.85
bluetoothd[123202]: src/main.c:parse_config() parsing /etc/bluetooth/main.conf
bluetoothd[123202]: src/main.c:parse_config_bool() General.FastConnectable = true
bluetoothd[123202]: src/main.c:parse_config_string() General.JustWorksRepairing = always
D-Bus setup failed: Name already in use
bluetoothd[123202]: src/main.c:main() Unable to get on D-Bus

Okay that's strange. So it's trying to claim a dbus name, and it's already in use?

What name? And what else would be using it, we just stopped bluetooth? After a bit of searching around, I was reminded that systemd services can subscribe to certain dbus names, and if a message arrives there, systemd will start the service. So what's happening is that we stopped bluetooth.service then something probably sent a message to the bus it registered to, and systemd started it again:

sh
$ systemctl status bluetooth.service

 bluetooth.service - Bluetooth service
     Loaded: loaded (/usr/lib/systemd/system/bluetooth.service; enabled; preset: disabled)
     Active: active (running) since Thu 2026-01-01 18:37:40 CET; 8min ago

Okay, so it's running again, confirming the above suspicion. We have to make sure it does not get restarted. Apparently the best way to do this is to mask the service, then stop it:

sh
$ systemctl mask bluetooth
$ systemctl stop bluetooth

$ /usr/lib/bluetooth/bluetoothd -n -d

bluetoothd[125553]: Bluetooth daemon 5.85
bluetoothd[125553]: src/main.c:parse_config() parsing /etc/bluetooth/main.conf
bluetoothd[125553]: src/main.c:parse_config_bool() General.FastConnectable = true
bluetoothd[125553]: src/main.c:parse_config_string() General.JustWorksRepairing = always
bluetoothd[125553]: src/adapter.c:adapter_init() sending read version command
bluetoothd[125553]: Starting SDP server
bluetoothd[125553]: src/sdpd-service.c:register_device_id() Adding device id record for 0002:1d6b:0246:0555
...

Okay perfect. After skimming through a few hundred logs there are a few lines that might be relevant:

bluetoothd[125553]: src/adapter.c:update_discovery_filter()
bluetoothd[125553]: src/adapter.c:discovery_filter_to_mgmt_cp()
bluetoothd[125553]: src/adapter.c:update_discovery_filter() filters were equal, deciding to not restart the scan.
bluetoothd[125553]: [:1.872:method_return] < [#125]
bluetoothd[125553]: [:1.872:method_call] > org.bluez.Adapter1.StartDiscovery [#126]
bluetoothd[125553]: src/adapter.c:start_discovery() sender :1.872
bluetoothd[125553]: [:1.872:error] < org.bluez.Error.InProgress [#126]
bluetoothd[125553]: src/shared/mgmt.c:can_read_data() [0x0000] event 0x0013
bluetoothd[125553]: src/adapter.c:discovering_callback() hci0 type 7 discovering 0 method 0
bluetoothd[125553]: src/adapter.c:trigger_start_discovery()
bluetoothd[125553]: src/adapter.c:cancel_passive_scanning()

Especially this line:

bluetoothd[125553]: [:1.872:method_call] > org.bluez.Adapter1.StartDiscovery [#126]
bluetoothd[125553]: src/adapter.c:start_discovery() sender :1.872

So this points to the cuplrit of :1.872 as well, and now it's just tracking down how to query the system what is behind that name. I actually started going down this way first, but completely missed the "sender :1.872" part, which made me chase the other solution.

First piece of new knowledge for the new year.