[BUG] macOS: main process pins one core at 100% while a wired Xbox controller is attached (Chromium XboxDataFetcher IOKit retry loop)
Summary
With an Xbox Elite Series 2 controller connected over USB, the Claude desktop app's main
(Electron browser) process spins one core at 100% for as long as the controller stays
plugged in. The spinning thread is Chromium's "Gamepad polling thread", stuck in a retry
loop trying to open the USB device through IOKit. Unplugging the controller drops the
process to idle within seconds. Reproduces deterministically on every plug-in.
The controller itself keeps working normally (macOS handles it), so the only symptom is
the wasted core: battery drain and heat on a laptop, and ~10% total CPU in Activity
Monitor on a 10-core machine.
Environment
- Claude desktop app 2.19675.0 (Electron 44.4.3, Chromium 152.0.7977.130 per the
Electron Framework binary), Claude Code 2.1.286 running in the Code tab
- macOS 26.7.1 (25G241), MacBook Air, Apple M5, 10 cores
- Controller: Microsoft Xbox Elite Series 2, wired USB (idVendor 0x045E / 1118,
idProduct 0x0B00 / 2816). The same controller is also paired over Bluetooth, which does
not trigger the issue.
- App state: Code tab with two sessions open; no in-app browser tab in use at the time.
Steps to reproduce
- Launch Claude desktop on macOS and leave it running.
- Plug an Xbox Elite Series 2 (or presumably any Microsoft USB controller in Chromium's
XboxDataFetcher product list) into the Mac by USB.
- Watch the
Claudeprocess (the main process, not a Helper) in Activity Monitor or
top -pid <pid>.
Expected: no measurable CPU change.
Actual: within 5 seconds the main process sits at ~100% of one core and stays there.
Unplugging the cable returns it to 0-6% immediately.
Measured on 2026-10-02:
| moment | main process CPU | hottest thread |
|---|---|---|
| controller attached for ~4 h | 98-100% | 98.5% (170 min system + 53 min user time) |
| 5 s after unplugging | 0.0 / 2.4 / 6.1% | 1.0% |
| 5 s after re-plugging | 100.1% | spinning again |
| +10 / +15 / +20 / +25 / +30 s | 101.4 / 100.3 / 100.4 / 99.6 / 100.2% | |
Diagnosis
sample <main pid> 3 while the controller was attached: of 2497 samples on the thread
named "Gamepad polling thread", 2363 were inside this path:
_CFRunLoopRunSpecificWithOptions
__CFRunLoopDoSource1
__CFMachPortPerform
IODispatchCalloutFromCFMessage (IOKit)
<Electron Framework> (XboxDataFetcher device-added / interest callback)
IOCreatePlugInInterfaceForService (IOKit)
IOUSBDeviceClass::start (IOUSBLib)
IORegistryEntryCreateCFProperties
Leaf frames on that thread are IOKit mach calls (io_connect_method,
io_registry_entry_get_property_bin_buf, io_service_open_extended,
io_service_add_interest_notification, io_service_close), which matches the thread's CPU
being mostly system time. The main thread and all other threads are idle (mach_msg wait).
A second 2-second sample taken right after re-plugging shows the same thread
(Thread_299107) in the same path.
Trimmed stack of that thread from the second sample (first 70 lines; full sample
output for both captures is available on request, about 440 KB each):
<details>
<summary>Gamepad polling thread, 2 s sample right after re-plugging</summary>
1657 Thread_299107: Gamepad polling thread
+ 1657 thread_start (in libsystem_pthread.dylib) + 8 [0x187af6c1c]
+ 1657 _pthread_start (in libsystem_pthread.dylib) + 136 [0x187afbc58]
+ 1657 v8::Isolate::InstallConditionalFeatures(v8::Local<v8::Context>) (in Electron Framework) + 50444 [0x11f0e87dc]
+ 1657 v8::internal::compiler::CompilationDependencies::FieldTypeDependencyOffTheRecord(v8::internal::compiler::MapRef, v8::internal::compiler::MapRef, v8::internal::InternalIndex, v8::internal::compiler::ObjectRef) const (in Electron Framework) + 359856 [0x11ff200e0]
+ 1657 v8::Value::IsObject() const (in Electron Framework) + 42416 [0x11fa4b2b8]
+ 1657 node::PrincipalRealm::inspector_disable_network_tracking() const (in Electron Framework) + 16036 [0x11f2f4964]
+ 1657 node::PrincipalRealm::inspector_disable_network_tracking() const (in Electron Framework) + 17652 [0x11f2f4fb4]
+ 1657 node::PerIsolatePlatformData::PostDelayedTaskImpl(std::__Cr::unique_ptr<v8::Task, std::__Cr::default_delete<v8::Task>>, double, v8::SourceLocation const&) (in Electron Framework) + 3576 [0x11fcc8eac]
+ 1654 v8::internal::compiler::CompilationDependencies::PrepareInstall() (in Electron Framework) + 310416 [0x1203b5064]
+ ! 1644 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] (in Foundation) + 212 [0x189403c34]
+ ! : 1614 _CFRunLoopRunSpecificWithOptions (in CoreFoundation) + 532 [0x187c8a508]
+ ! : | 1550 __CFRunLoopRun (in CoreFoundation) + 2168 [0x187bb7e18]
+ ! : | + 1550 __CFRunLoopDoSource1 (in CoreFoundation) + 504 [0x187bb9450]
+ ! : | + 1550 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ (in CoreFoundation) + 60 [0x187bb9528]
+ ! : | + 1550 __CFMachPortPerform (in CoreFoundation) + 288 [0x187be448c]
+ ! : | + 1501 IODispatchCalloutFromCFMessage (in IOKit) + 332 [0x18be622d0]
+ ! : | + ! 1493 ares_llist_node_next (in Electron Framework) + 15229780 [0x124fbd35c]
+ ! : | + ! : 831 ares_llist_node_next (in Electron Framework) + 15229000 [0x124fbd050]
+ ! : | + ! : | 760 ares_llist_node_next (in Electron Framework) + 15222964 [0x124fbb8bc]
+ ! : | + ! : | + 607 IOCreatePlugInInterfaceForService (in IOKit) + 1564 [0x18be6aab4]
+ ! : | + ! : | + ! 341 IOUSBDeviceClass::start(__CFDictionary const*, unsigned int) (in IOUSBLib) + 144 [0x16707d6c4]
+ ! : | + ! : | + ! : 242 IORegistryEntryCreateCFProperties (in IOKit) + 156 [0x18be6026c]
+ ! : | + ! : | + ! : | 242 io_registry_entry_get_properties_bin_buf (in IOKit) + 160 [0x18bf027c4]
+ ! : | + ! : | + ! : | 242 mach_msg2_internal (in libsystem_kernel.dylib) + 76 [0x187aca5a4]
+ ! : | + ! : | + ! : | 242 mach_msg2_trap (in libsystem_kernel.dylib) + 8 [0x187ab7c34]
+ ! : | + ! : | + ! : 99 IORegistryEntryCreateCFProperties (in IOKit) + 200 [0x18be60298]
+ ! : | + ! : | + ! : 36 IOCFUnserializeBinary (in IOKit) + 908 [0x18be608b0]
+ ! : | + ! : | + ! : + 15 __CFStringCreateImmutableFunnel3 (in CoreFoundation) + 2100 [0x187b3dde8]
+ ! : | + ! : | + ! : + ! 5 _CFRuntimeCreateInstance (in CoreFoundation) + 496 [0x187b3e790]
+ ! : | + ! : | + ! : + ! : 2 _CFAllocatorAllocateImpl (in CoreFoundation) + 12,108 [0x187c50abc,0x187c50b1c]
+ ! : | + ! : | + ! : + ! : 2 v8::internal::compiler::CompilationDependencies::PrepareInstall() (in Electron Framework) + 318052 [0x1203b6e38]
+ ! : | + ! : | + ! : + ! : | 1 v8::internal::compiler::CompilationDependencies::PrepareInstall() (in Electron Framework) + 252916 [0x1203a6fc8]
+ ! : | + ! : | + ! : + ! : | + 1 v8::Object::SetLazyDataProperty(v8::Local<v8::Context>, v8::Local<v8::Name>, void (*)(v8::Local<v8::Name>, v8::PropertyCallbackInfo<v8::Value> const&), v8::Local<v8::Value>, v8::PropertyAttribute, v8::SideEffectType, v8::SideEffectType) (in Electron Framework) + 63100 [0x11e082130]
+ ! : | + ! : | + ! : + ! : | + 1 _platform_memset (in libsystem_platform.dylib) + 180 [0x187b05144]
+ ! : | + ! : | + ! : + ! : | 1 v8::internal::compiler::CompilationDependencies::PrepareInstall() (in Electron Framework) + 316356 [0x1203b6798]
+ ! : | + ! : | + ! : + ! : 1 __CFAllocatorSystemAllocateTyped (in CoreFoundation) + 52 [0x187c509e4]
+ ! : | + ! : | + ! : + ! : 1 DYLD-STUB$$malloc_default_zone (in CoreFoundation) + 8 [0x187d37ce0]
+ ! : | + ! : | + ! : + ! 5 _CFRuntimeCreateInstance (in CoreFoundation) + 392,256,... [0x187b3e728,0x187b3e6a0,...]
+ ! : | + ! : | + ! : + ! 2 _CFRuntimeCreateInstance (in CoreFoundation) + 840 [0x187b3e8e8]
+ ! : | + ! : | + ! : + ! : 1 objc_object::changeIsa(objc_class*) (in libobjc.A.dylib) + 76 [0x1876ab8bc]
+ ! : | + ! : | + ! : + ! : 1 object_setClass (in libobjc.A.dylib) + 36 [0x1876949a0]
+ ! : | + ! : | + ! : + ! 1 _CFAllocatorAllocateImpl (in CoreFoundation) + 220 [0x187c50b8c]
+ ! : | + ! : | + ! : + ! 1 _CFRuntimeCreateInstance (in CoreFoundation) + 472 [0x187b3e778]
+ ! : | + ! : | + ! : + ! : 1 __CFAllocatorRespectsHintZeroWhenAllocating (in CoreFoundation) + 28 [0x187b3e948]
+ ! : | + ! : | + ! : + ! 1 v8::internal::compiler::CompilationDependencies::PrepareInstall() (in Electron Framework) + 318080 [0x1203b6e54]
+ ! : | + ! : | + ! : + 9 __CFStringCreateImmutableFunnel3 (in CoreFoundation) + 2768 [0x187b3e084]
+ ! : | + ! : | + ! : + ! 5 __CFSearchStringROM (in CoreFoundation) + 44 [0x187b3e348]
+ ! : | + ! : | + ! : + ! : 5 __CFStrHashEightBit2 (in CoreFoundation) + 136,116 [0x187b3e444,0x187b3e430]
+ ! : | + ! : | + ! : + ! 4 __CFSearchStringROM (in CoreFoundation) + 16,56,... [0x187b3e32c,0x187b3e354,...]
+ ! : | + ! : | + ! : + 8 __CFStringCreateImmutableFunnel3 (in CoreFoundation) + 192,536,... [0x187b3d674,0x187b3d7cc,...]
+ ! : | + ! : | + ! : + 2 __CFStringCreateImmutableFunnel3 (in CoreFoundation) + 216 [0x187b3d68c]
+ ! : | + ! : | + ! : + ! 2 _CFGetTSDCreateIfNeeded (in CoreFoundation) + 12,44 [0x187b3fcf8,0x187b3fd18]
+ ! : | + ! : | + ! : + 2 __CFStringCreateImmutableFunnel3 (in CoreFoundation) + 2656 [0x187b3e014]
+ ! : | + ! : | + ! : + 2 _platform_memmove (in libsystem_platform.dylib) + 0,428 [0x187b05360,0x187b0550c]
+ ! : | + ! : | + ! : 27 IOCFUnserializeBinary (in IOKit) + 1396 [0x18be60a98]
+ ! : | + ! : | + ! : + 8 -[__NSDictionaryM __setObject:forKey:] (in CoreFoundation) + 392 [0x187b59f0c]
+ ! : | + ! : | + ! : + ! 7 __CFStringHash (in CoreFoundation) + 232,952,... [0x187b430bc,0x187b4338c,...]
+ ! : | + ! : | + ! : + ! 1 -[NSTaggedPointerString hash] (in CoreFoundation) + 84 [0x187b5ab94]
+ ! : | + ! : | + ! : + 7 -[__NSDictionaryM __setObject:forKey:] (in CoreFoundation) + 444 [0x187b59f40]
+ ! : | + ! : | + ! : + ! 2 -[NSTaggedPointerString isEqual:] (in CoreFoundation) + 100 [0x187b508c4]
+ ! : | + ! : | + ! : + ! : 1 _NSIsNSString (in CoreFoundation) + 180 [0x187b509ac]
+ ! : | + ! : | + ! : + ! : | 1 DYLD-STUB$$object_getMethodImplementation (in CoreFoundation) + 12 [0x187d382b4]
+ ! : | + ! : | + ! : + ! : 1 object_getMethodImplementation (in libobjc.A.dylib) + 388 [0x187696760]
+ ! : | + ! : | + ! : + ! 2 -[__NSCFString isEqual:] (in CoreFoundation) + 160,232 [0x187b42948,0x187b42990]
+ ! : | + ! : | + ! : + ! 1 -[NSTaggedPointerString isEqual:] (in CoreFoundation) + 0 [0x187b50860]
+ ! : | + ! : | + ! : + ! 1 -[__NSCFString isEqual:] (in CoreFoundation) + 276 [0x187b429bc]
+ ! : | + ! : | + ! : + ! : 1 __CFStringEqual (in CoreFoundation) + 64 [0x187c44aec]
+ ! : | + ! : | + ! : + ! 1 isEqualToString (in CoreFoundation) + 100 [0x187b50a9c]
+ ! : | + ! : | + ! : + ! 1 objc_msgSend (in libobjc.A.dylib) + 56 [0x187691838]
</details>
Likely root cause (from current Chromium source, not verified against 152 line by line)
- On macOS,
device/gamepad/gamepad_platform_data_fetcher.haddsXboxDataFetcher
unconditionally, alongside GameControllerDataFetcherMac. There is no base::Feature or
switch that disables XboxDataFetcher; the only related flag,
XboxUseGameControllerDataFetcherMac (enabled by default, and present in the Claude
binary), is not consulted inside xbox_data_fetcher_mac.cc.
XboxDataFetcherregisters an IOKit matching notification for Microsoft product IDs
including 0x0B00 and calls TryOpenDevice for each match. Since macOS 11 the OS's own
driver (gamecontrollerd / GameController framework) holds the device, so the open fails
with exclusive access. The fetcher then registers an interest notification and retries
on kIOMessageServiceWasClosed.
- The observed behaviour is that retry firing continuously: every callback re-runs
IOCreatePlugInInterfaceForService + IOUSBDeviceClass::start, fails, re-registers,
and the next notification arrives at once. Net effect: a busy loop driven by IOKit
messages for as long as the device is present.
- The gamepad polling thread only exists once a renderer has bound the Gamepad service
(navigator.getGamepads() or a gamepadconnected listener). Something in the app's
renderer does this at startup, because the loop starts within seconds of plugging in,
with no user interaction in the app.
Suggested fixes
- App side (no Chromium change needed): make sure no renderer page in the app binds the
Gamepad API unless a feature actually needs it. If no page starts the GamepadProvider,
XboxDataFetcher is never created and the USB notifications are never registered.
- Upstream: have
XboxDataFetcherskip Xbox product IDs when
XboxUseGameControllerDataFetcherMac is enabled (the GameController path already
handles them), or add exponential backoff / a retry cap to the exclusive-access retry.
Workaround for users
Unplug the controller, or quit Claude desktop while using a wired Xbox controller. Pairing
the controller over Bluetooth avoids the USB fetcher entirely, at the cost of Bluetooth
input latency.