watchOS CoreBluetooth: BLE link stability, AutoReconnect and reduced reconnection range

I have a watchOS app communicating with a BLE peripheral during an active HKWorkoutSession.

The workout provides sufficient runtime: once connected, the Watch can receive a notifying GATT characteristic approximately once per minute for hours, including while the display is off. In normal conditions the connection is very stable.

The difficult case is outdoor use with marginal RF conditions. The peripheral and application protocol otherwise work reliably, but outdoors there are fewer reflected RF paths and normal body movement can intermittently obstruct the Watch-to-peripheral path. In those situations the link occasionally terminates with CBError.connectionTimeout.

Reconnection is particularly important because this peripheral advertises for reconnection only approximately once per minute. We also know approximately which second of each minute the peripheral becomes active, so missing a single advertising opportunity can cause a relatively long data gap.

I’m trying to better understand the supported CoreBluetooth/watchOS behavior in this situation.

  1. Maintaining a marginal existing connection

Once the BLE connection is established, are there any public CoreBluetooth mechanisms or recommended practices for making that connection more robust against CBError.connectionTimeout?

In particular, can application-level GATT activity have any influence on connection scheduling or link robustness, or is this entirely handled by the Bluetooth stack below CoreBluetooth?

  1. CBConnectPeripheralOptionEnableAutoReconnect with sparse advertising

When CBConnectPeripheralOptionEnableAutoReconnect is enabled and the connection is lost, how does watchOS handle a peripheral that advertises only very infrequently?

Does CoreBluetooth simply keep the reconnect pending and wait for subsequent advertisements indefinitely, subject to system policy?

  1. Known peripheral activity timing

If the app knows approximately when the peripheral becomes active and advertises each minute, is there any supported way to make use of that timing during reconnection?

Or should the app simply leave AutoReconnect pending and let CoreBluetooth handle reception?

  1. WWDC22 reduced reconnection range

WWDC22 session 10135 says:

“If your device is on the edge of Bluetooth range and repeatedly disconnects while in Background BLE connection, the reconnection range will be reduced. This means only devices close to the Apple Watch will reconnect.”

The session also says these limits are counted over a rolling 24-hour window and are reset when the user interacts with the app.

I would particularly appreciate clarification of how this works in practice:

  • What conditions cause a disconnect to count toward this policy?
  • Does this policy apply while an app has an active HKWorkoutSession and continues executing in the background?
  • Is the state maintained per peripheral, per app, or more globally?
  • What exactly constitutes the user interaction that resets it?
  • Does bringing an already-running workout app back to the foreground reset it, or does the app need to be relaunched?
  • Is there any public API or diagnostic indication that reduced reconnection range is currently active?
  • Is there any supported way to explicitly clear/reset that state?
  1. Connection parameters

Does watchOS expose any supported mechanism for a CoreBluetooth central to influence parameters relevant to connection robustness, such as connection interval, supervision timeout, PHY, transmit power, or similar link parameters?

  1. Diagnosing CBError.connectionTimeout

When CoreBluetooth reports CBError.connectionTimeout, is there any supported diagnostic method to determine whether the Watch stopped receiving the peripheral, the peripheral stopped receiving the Watch, or whether this should simply be treated as a link-layer timeout without attribution to either side?

For context, this is not primarily a background-runtime problem. The active workout keeps the app running and the normal persistent BLE connection can operate flawlessly for many hours. I’m specifically trying to understand the best supported strategy for preserving and recovering the BLE link when RF conditions temporarily become marginal, especially with a peripheral that advertises very sparsely after a disconnect.

DTS asked me to post this question here for review.

I promise that if you fix this problem, I'll buy that fantastic, amazing, and one-of-a-kind watch—even though it has those stupid restrictions imposed by extraordinary smart peoples. Until then, I love and I stick to my Pixel Watch.

So, let me actually jump straight to here:

For context, this is not primarily a background-runtime problem. The active workout keeps the app running, and the normal persistent BLE connection can operate flawlessly for many hours.

Across the system, most of these issues/limitations are about how the system handles things when your app ISN'T awake. The issue is that the main power benefit of policies like this:

“If your device is on the edge of Bluetooth range and repeatedly disconnects while in Background BLE connection, the reconnection range will be reduced. This means only formatting close to the Apple Watch will reconnect.”

...ISN'T the power saved by the cost of any individual handshake connection, as the direct power cost is fairly trivial. The actual issue (and savings) here is that the process of reconnecting requires waking the system and your app, both of which are relatively high cost.

However, that same logic also applies in reverse: that is, if your app is already awake, artificially throttling your Bluetooth activity doesn't actually provide very much benefit, as the primary power drain is simply that your app is awake, NOT your Bluetooth activity.

In any case, given that you know this:

Reconnection is particularly important because this peripheral advertises for reconnection only approximately once per minute. We also know approximately which second of each minute the peripheral becomes active, so missing a single advertising opportunity can cause a relatively long data gap.

If you know this within a reasonable degree of accuracy, then the simplest solution is to stopScan when you know the connection is lost, then scanForPeripheralsWithServices again when you know the time window is reopening. How well that will work depends a lot on the specifics of your accessory, but I'd expect it to work better than simply doing this:

Or should the app simply leave AutoReconnect pending and let CoreBluetooth handle reception?

I think one thing to understand here is that a very low advertising rate will mean your accessory will basically look EXACTLY like an accessory that is at the "edge" of the device’s range and then coming/going. Instead of trying to avoid that scenario, I think you'll probably get better results by directly scanning again if/when you get disconnected.

When CoreBluetooth reports CBError.connectionTimeout, is there any supported diagnostic method to determine whether the Watch stopped receiving the peripheral, the peripheral stopped receiving the Watch, or whether this should simply be treated as a link-layer timeout without attribution to either side?

In practice, I think an app’s only option is to do this:

whether this should simply be treated as a link-layer timeout without attribution to either side?

The problem here is that the radio data alone isn't really enough to provide meaningful diagnostic data, as the external environment can basically make either case look like the other.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks, Kevin. That was a helpful explanation of the reasoning, and I tried exactly what you suggested.

What I implemented

  • AutoReconnect disabled entirely. connect() is called without CBConnectPeripheralOptionEnableAutoReconnect.
  • On disconnect: stopScan(). About 3 s before the peripheral's known advertising time, a fresh scanForPeripherals(withServices:) (service UUID filter, no duplicates).
  • On discovery: connect() immediately. If not connected ~10 s after the expected advertising time: cancel the pending connect, stopScan(), repeat at the next minute.
  • watchOS 26.6 (23U67), active HKWorkoutSession, Bluetooth logging profile installed. bluetoothd reported the app as background-running during reconnection.

Log lines below are trimmed, identifiers replaced by placeholders, times are local.

Result 1: Timed scanning works, but offers no apparent reception advantage

The peripheral's advertising time stayed stable within 0.1 s across all disconnects, so the scan windows were correctly positioned. bluetoothd performed passive scanning with a 30 ms window every 300 ms:

[LE.Scan] Starting passive scan (300.00ms/30.00ms) with duplicate filter enabled

For comparison, each pending connect() uses LE Extended Create Connection with 30/30 ms for the first ~6 s, then 300/30 ms. Thus, after the initial connection phase, both approaches use the same nominal scan interval/window.

When a scan window missed the peripheral, no advertising report from it appeared in the controller logs.

Result 2: The reduced reconnection range also applies to explicit scan + connect

All five link losses were supervision timeouts on connections established without AutoReconnect.

At the fifth disconnect, bluetoothd's disconnection history contained:

11:56:14.778 [LE.Connection] reason 708 time:2026-10-09 11:34:37.574
11:56:14.778 [LE.Connection] reason 708 time:2026-10-09 11:45:00.505
11:56:14.778 [LE.Connection] reason 708 time:2026-10-09 11:49:07.382
11:56:14.778 [LE.Connection] reason 708 time:2026-10-09 11:52:22.903
11:56:14.778 [LE.Connection] reason 708 time:2026-10-09 11:56:14.772

The crucial finding is that the reconnection restriction is applied even after a successful explicit scan discovery.

The app discovered the peripheral at -76 dBm and called connect() ~30 ms later:

11:57:24.374 [LE.Scan] Matched UUID <service> for device "<peripheral>"
11:57:24.406 [XPC] Received XPC message "CBMsgIdConnectPeripheral" from session "<app>-central"
11:57:24.406 [App] Connecting device "<peripheral>" for session "<app>-central" with options (connect:0 disconnect:0 notify:0 delay:0 ...)
11:57:24.406 [App] Device <peripheral> classicPaired 0, lePaired 0, discoveredViaLeScan 1
11:57:24.408 [LE.Connection] updateLeConnectionRSSIThresholdState for <peripheral> from 0 to 1
11:57:24.408 [LE.Connection] device "<peripheral>" ... filter accept list options: conn_cmpt_option=0, minRSSI=-70, PHYs=0

That connection never completed. Earlier in the same session, before the restriction engaged, connections at -86 and -94 dBm had succeeded.

Toggling Bluetooth off/on immediately cleared the state. The next connection, at -74 dBm, completed within 40 ms:

11:57:52.301 [Core] Powering off chipset
11:57:56.285 [Core] Powering on chipset
11:58:24.401 [LE.Connection] statedump: LeConnectionRSSIThresholdState History: 0 entries

Result 3: The restriction uses a 20,864-second counting window

On every connection attempt, bluetoothd logs:

getNumDisconnectionsBySignalQuality for <peripheral> over time window 20864.000000 count N

This corresponds to 5 h 47 min 44 s, and the same window value appears in our sysdiagnoses from September 29, September 30, and October 9.

On October 9, the count reached 5 at 11:57:24.408, precisely when updateLeConnectionRSSIThresholdState changed from 0 to 1.

We also observed the restriction clearing without user intervention: on September 29 it was active at 10:14, but at the next connection attempt at 18:02 the count was 0 and the threshold state returned to 0.

We haven't observed an individual disconnect aging out at exactly 20,864 seconds, but the logged window and observed expiration are consistent.

Interestingly, WWDC22 describes a rolling 24-hour window. We don't know whether this is a different mechanism or whether the implementation has changed.

Result 4: User interaction does not reset the restriction

In earlier runs with AutoReconnect enabled, the same restriction survived:

  • Bringing the app to the foreground and interacting with it
  • Navigating between views
  • Returning to the Home Screen and reopening the app
  • Ending and restarting the workout
  • Force-quitting and relaunching the app

Only toggling Bluetooth (or Airplane Mode with Bluetooth disabled) cleared it immediately. Otherwise, the restriction eventually expired.

Our own cancelPeripheralConnection disconnects (reason 722) did not contribute to the counter; supervision timeouts (reason 708) did.

This appears inconsistent with WWDC22 session 10135, which states that the reduced-reconnection-range limits are reset whenever the user interacts with the app.

Questions

  1. Is this restriction intended to apply during an active HKWorkoutSession? As you explained, throttling Bluetooth activity provides little benefit while the app is already executing. Here, bluetoothd reports the app as background-running, and it explicitly initiates connect() immediately after discovering the peripheral.

  2. Is there any supported way to avoid, detect, or reset this restriction? WWDC22 describes the mechanism in the context of Background BLE connections, with a rolling 24-hour window and a reset on user interaction. Our observations differ on all three points. Is this the same mechanism, or a different restriction?

  3. Is this expected behavior or a watchOS bug? In particular, should the failure of the documented user-interaction reset be treated as a bug?

I have already filed Feedback Assistant report FB25101044, referencing DTS case 22079163 and including an earlier sysdiagnose captured with Apple's Bluetooth logging profile.

I also have the newer October 9 sysdiagnose documenting the explicit scan/connect experiment described above, including the threshold activation and Bluetooth reset. I can provide that through Feedback Assistant or DTS if useful.

Thanks again.