Why I'm Obsessed With USB Hotplug Detection (And Why You Should Be Too)
Admin User
Author
Last month, I spent three hours debugging a file sync application because my refresh button wasn't working. Not because the button had a bug, but because I'd built the entire feature around expecting users to click it. When a USB device connected, nothing happened until someone manually triggered the sync. It was friction I'd normalized so completely that I didn't even see it anymore.
Then I saw someone using an app that just... worked. They plugged in their phone, and within a second the UI updated. No button. No "try again." No asking themselves, "did it actually detect this?" That's when I realized I'd been shipping a half-finished experience.
Reading about how to properly handle USB hotplug detection on macOS—specifically in Rust—made me understand why that gap exists for so many developers. It's not that the solution is impossible; it's that most of us never dig deep enough into the platform-specific APIs to implement it right. I want to walk through what I've learned about doing this properly.
The Problem Nobody Talks About
Here's the thing about device detection: it's not sexy work. It doesn't ship features, it doesn't impress in a demo, and it doesn't go in a changelog. But it's the difference between an app that feels native and responsive versus one that feels... reluctant.
I've built integrations with external hardware before. Mobile phones, Arduinos, USB adapters. Every time, I defaulted to some version of polling—a timer that periodically checks for connected devices. It works, it's simple, and it burns battery life and CPU cycles while making everything feel slightly delayed.
The right way is hotplug detection: telling the OS, "Hey, let me know the instant a USB device connects or disconnects." On macOS, that means tapping into IOKit, Apple's kernel framework for hardware communication.
How Rust + nusb Makes This Practical
The original article breaks down using nusb, a Rust library that wraps IOKit's USB notifications. Instead of constantly asking "are you there yet?", you subscribe to an event stream and react when something actually happens.
The pattern is straightforward: spawn a dedicated thread that blocks on a watch iterator, match against connect/disconnect events, and emit those events up to your application layer. On macOS, IOKit handles all the hard work of monitoring the USB bus; you're just listening.
What I find elegant about this approach is the separation of concerns. Your hardware detection thread doesn't need to know anything about your UI. It just says "device X connected" and your app decides what to do with that information. In Tauri (an Electron alternative for Rust desktop apps), that means emitting an event to the frontend.
The Two-Step Verification That Changed My Mind
Here's where the original article's approach gets smart, and where I initially had doubts: they don't just trust the USB vendor ID. They verify with ADB (Android Debug Bridge) afterward.
At first, I thought this was overkill. You catch a USB connection, you look at the vendor ID, done. But then I thought about the edge cases: what if someone has an external hard drive from a Chinese manufacturer that used a vendor ID that collides? What if there's a USB hub situation causing false positives?
The author's approach is vendor ID for speed (instant visual feedback), then ADB for accuracy (don't actually try to sync with a printer). This is pragmatic design—give the user something responsive while you verify in the background.
fn watch_usb_devices() {
std::thread::spawn(move || {
let watch = nusb::watch_devices().expect("Failed to start USB watch");
for event in watch {
match event {
HotplugEvent::Connected(device_info) => {
if ANDROID_VENDOR_IDS.contains(&device_info.vendor_id()) {
// Fast feedback to user
println!("Device connected: {}",
device_info.product_string().unwrap_or_default());
// Verify with ADB in background
if confirm_with_adb().await {
emit_safe_event("device-confirmed");
}
}
}
HotplugEvent::Disconnected(_) => {
emit_safe_event("device-disconnected");
}
}
}
});
}
The blocking iterator means this thread is sleeping when nothing's happening. It only wakes up when IOKit detects actual USB events. Compare that to a polling loop that wakes up every 500ms, and suddenly the battery and CPU impact makes sense.
My Real Take
I love this article because it's rooted in shipping real apps. The author shipped seven macOS applications as a solo developer—that's not theoretical, that's battle-tested. They know which approach is worth the implementation effort because they've felt the user impact.
Would I implement this exactly the same way? Maybe not. I'd probably want to add retry logic for ADB confirmation and better error handling when the watch thread encounters issues. But the core philosophy—"use platform notifications, verify for safety, emit to your app layer"—that's solid.
The thing I'm stealing immediately: the two-step approach. Fast feedback from hardware, confirmed by software. That's the pattern.
What Would You Do?
Have you built device detection into a desktop app? What did you choose—polling, platform notifications, or something else entirely? I'm curious whether other developers have hit the same friction I did, and what made them finally care about getting it right.
Source: This post was inspired by "USB Hotplug Detection in Rust on macOS — Reacting to Device Connect/Disconnect" by Dev.to. Read the original article