React Native BLE Device Scanning That Doesn’t Miss Devices (filters, duplicates, and scan windows)

React Native BLE Device Scanning That Doesn’t Miss Devices (filters, duplicates, and scan windows)

Dec 12, 2025
8 minute read

If BLE scanning feels random, it’s usually not your imagination. A device advertises for a tiny slice of time, your phone’s OS decides when to listen, and your app is trying to catch that moment while also rendering UI, handling permissions, and sometimes connecting.

Reliable React Native BLE scanning comes down to three things you can control: how long you scan (scan windows), how you treat repeated advertisements (duplicates), and how aggressively you filter (often too aggressively). This guide focuses on react-native-ble-plx, with patterns you can copy into production.

Why devices “disappear” during scanning

Think of BLE advertising like a lighthouse that blinks. If you only look for one second, you might miss the blink, even though the lighthouse is working.

The most common reasons scans miss devices:

  • Scan stops too quickly (or is interrupted by a connect attempt).
  • Hard filters block devices whose advertisements don’t match what you expect.
  • Duplicates are disabled, so you lose RSSI updates and changing advertisement payloads.
  • OS behavior differs across Android 12 to 15 and current iOS versions, especially with battery and background limits.

For library specifics, keep the BleManager.startDeviceScan signature handy in the official docs at <https://dotintent.github.io/react-native-ble-plx/>.

Start with a clean baseline (state, permissions, and expectations)

Before tuning scan logic, confirm the basics:

1) Wait for PoweredOn. Start scanning only after the manager reports PoweredOn. If you kick off a scan while Bluetooth is toggling, you can get empty results or errors.

2) Treat permissions as part of scanning, not an afterthought. On Android 12+, scanning is tied to the Bluetooth runtime permissions (for example BLUETOOTH_SCAN), and OEM builds still sometimes behave oddly if system location services are off. Android’s current guidance is in the official “Find BLE devices” doc: <https://developer.android.com/develop/connectivity/bluetooth/ble/find-ble-devices>.

3) Decide what “miss” means in your product. If you must find a device within 3 seconds, you need longer scan-on time, fewer early filters, and probably duplicates enabled. If you can wait 15 seconds, you can be more selective and battery-friendly.

A scan session manager with configurable scan windows (the pattern that fixes “it works on my phone”)

Continuous scanning is tempting, but it often causes trouble: battery drain, throttling, and conflicts with connection flows. A better approach is a simple scan scheduler that runs in bursts.

Here’s a TypeScript-friendly “scan session manager” sketch you can adapt without turning your codebase into a BLE science project:

ScanWindowConfig (example):

  • scanOnMs: how long to scan each burst (example: 4000 to 8000)
  • scanOffMs: pause between bursts (example: 1000 to 3000)
  • totalMs: overall session limit (example: 20000)
  • allowDuplicates: optional, default true for provisioning flows

Manager responsibilities:

  • Own the single active scan (avoid starting two scans).
  • Stop scanning before connecting.
  • Track elapsed time and end cleanly.

Operational outline (using react-native-ble-plx):

  • Keep isScanning in state.
  • On startSession(config):
  • Record sessionStart = Date.now().
  • Run scanBurst() immediately.
  • scanBurst():
  • Call manager.startDeviceScan(serviceUUIDsOrNull, { allowDuplicates }, callback).
  • Set a timer to call manager.stopDeviceScan() after scanOnMs.
  • After stopping, if Date.now() - sessionStart < totalMs, wait scanOffMs then scan again.
  • On stopSession():
  • Clear timers.
  • Call manager.stopDeviceScan() once.
  • Mark session finished and flush metrics.

This windowed approach usually finds “shy” devices because you’re listening across multiple advertisement intervals, not gambling on one short scan.

For react-native-ble-plx scanning options and behavior notes, the project wiki is practical: <https://github.com/dotintent/react-native-ble-plx/wiki/Bluetooth-Scanning>.

Optional allowDuplicates, and why “false” is often the wrong default

Many apps set allowDuplicates: false and then wonder why RSSI is noisy, stale, or missing. With duplicates off, you’ll often get one callback per device, then nothing. That’s fine for a quick picker list, but weak for provisioning, proximity UI, or “tap the nearest device” flows.

A good rule:

  • Provisioning / nearby / RSSI-based ranking: set allowDuplicates: true, dedupe in-app.
  • Simple list to select any device: allowDuplicates: false can be fine, but still consider a short “refresh” burst.

Also note that some peripherals rotate advertisement payload content over time (service data, manufacturer data, flags). If you only keep the first packet you see, you might miss the one that contains the identifier you needed.

A de-dup layer that still updates RSSI and advertisement payload

You want one row per device in your UI, but you also want fresh RSSI and payload. That calls for a tiny in-memory registry.

A practical de-dup model:

  • Key by device.id first (fast and simple).
  • Store:
  • firstSeenAt, lastSeenAt
  • lastRssi
  • name (if present)
  • manufacturerData, serviceData (latest values)
  • advCount (how many callbacks you got)

Update rules (simple and effective):

  • If device key is new, insert and notify UI.
  • If device key exists, update lastSeenAt, lastRssi, and overwrite payload fields when present.
  • Only re-render your list when:
  • RSSI changed by a threshold (example: 5 to 10 dBm), or
  • name/payload changes, or
  • it hasn’t been updated in N milliseconds.

This keeps the UI stable while still reacting to movement and changing advertisements.

Handle device address randomization, or your “unique key” won’t be unique

Some devices use privacy features that rotate addresses. On iOS you might never see a true MAC address, and on Android a device identifier can still change in ways that surprise you. If you must recognize a device across sessions, don’t bet your product on addresses.

Instead, prefer a stable identifier inside the advertisement (often manufacturer data or service data). For background and privacy context, this overview helps: <https://novelbits.io/how-to-protect-the-privacy-of-your-bluetooth-low-energy-device/>.

Filtering best practices (collect first, filter second)

Service UUID filters look clean on paper, but they can hide real devices:

  • Some peripherals don’t advertise services all the time.
  • Some advertise a shortened list until a button press.
  • Some advertise only manufacturer data, then reveal services after connection.

So the default recommendation for reliable discovery is:

Start broad, then filter in-app after you’ve collected results for at least one full scan window.

A safer filtering order that misses fewer devices:

  1. Session-level gating: only show devices seen in the last X seconds.
  2. Soft match: local name prefix, manufacturer data signature, or service data pattern.
  3. Hard match: service UUIDs, exact model IDs, firmware flags.

On Android, parsing manufacturer data can differ by vendor and payload format. Treat manufacturerData as bytes, not as a string, and write tests with real samples captured from devices. If you don’t, you’ll “filter out” the right device because your parser is wrong.

Logging and metrics that expose missed devices (before users do)

When scanning is unreliable, feelings aren’t enough. Add lightweight metrics so you can compare devices, OS versions, and build changes.

Track these per scan session:

  • scanSessionId and config (scanOnMs, scanOffMs, totalMs, allowDuplicates)
  • callbacksTotal
  • uniqueDevicesCount
  • topDevicesByAdvCount (helps spot OS throttling or filter mistakes)
  • timeToFirstDeviceMs
  • errors by code (and whether Bluetooth was PoweredOn)

If you see callbacksTotal drop sharply on one Android model, it’s usually an OS behavior or permission state, not your filtering logic. For Android-specific scan failure patterns and error causes, this troubleshooting write-up is worth keeping around: <https://punchthrough.com/android-ble-scan-errors/>.

Common pitfalls that cause “random” scan results

Scanning while connecting

Stop scanning before you connect. Many stacks behave better when the radio isn’t doing scan and connection setup at the same time. If your UX needs “keep scanning,” pause, connect, then resume bursts after the connection stabilizes.

Stopping the scan too quickly

A 1 to 2 second scan is a coin flip. Use a scan-on window that’s long enough to overlap multiple ad intervals. In practice, 4 to 8 seconds is a strong starting point for provisioning screens.

Relying only on service UUID filters

Use service filters after you’ve confirmed your devices always advertise those UUIDs. When in doubt, scan with null services, then filter in the callback.

Android 12 to 15 permission and background realities

Foreground scanning is usually fine when permissions are correct. Background scanning is where things tighten up. If your use case truly needs background behavior, read Android’s background communication guidance and design for it, not around it: <https://developer.android.com/develop/connectivity/bluetooth/ble/background>.

iOS scanning expectations

iOS prioritizes user privacy and battery, and it won’t always behave like Android. Apple’s Core Bluetooth guide is old but still explains the model and constraints well: <https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth%5Fconcepts/AboutCoreBluetooth/Introduction.html>.

Conclusion

Reliable React Native BLE scanning is less about one magic option and more about a repeatable process: scan in windows, allow duplicates when you need live updates, dedupe in-app, and filter later than you think. Add basic metrics, and “it sometimes misses devices” turns into a bug you can measure and fix.

If your scanner feels flaky today, start by implementing scan windows and a de-dup registry, then tighten filters only after you’ve seen real advertisement data from real devices.