Battery Optimization Best Practices
Battery life is a critical concern for BLE applications, especially on mobile devices. This guide provides strategies to minimize power consumption while maintaining functionality and responsiveness.
Table of Contents
- Scan Mode Selection
- Scan Duration Management
- Background Scanning
- Connection Priority Optimization
- MTU Optimization
- Service Caching
- Notification Management
- Platform-Specific Optimizations
Scan Mode Selection
The scan mode significantly impacts battery consumption. Choose the appropriate mode based on your use case.
Scan Mode Comparison
| Mode | Power Consumption | Discovery Latency | Use Case |
|---|---|---|---|
| LowPower | Low | ~5 seconds | Background monitoring, passive scanning |
| Balanced | Medium | ~2 seconds | General purpose, default mode |
| LowLatency | High | < 1 second | Active device search, time-critical |
| Opportunistic | Minimal | Unpredictable | Background apps (Android only) |
Choosing the Right Mode
public class BatteryOptimizedScanner
{
private readonly IBluetoothScanner _scanner;
public async Task ScanForDeviceAsync(DeviceSearchPriority priority)
{
var options = new ScanningOptions
{
ScanMode = priority switch
{
// User is actively waiting - fast discovery
DeviceSearchPriority.Immediate => BluetoothScanMode.LowLatency,
// Background monitoring - preserve battery
DeviceSearchPriority.Background => BluetoothScanMode.LowPower,
// User can wait a bit - balanced approach
_ => BluetoothScanMode.Balanced
},
// Filter out noise
IgnoreNamelessAdvertisements = true,
IgnoreDuplicateAdvertisements = priority == DeviceSearchPriority.Background
};
await _scanner.StartScanningAsync(options);
}
}
public enum DeviceSearchPriority
{
Immediate,
Normal,
Background
}
Dynamic Scan Mode Adjustment
public class AdaptiveScanManager
{
private readonly IBluetoothScanner _scanner;
private DateTime _scanStartTime;
private bool _deviceFound;
public async Task ScanWithDynamicModeAsync(CancellationToken cancellationToken = default)
{
_scanStartTime = DateTime.UtcNow;
_deviceFound = false;
// Start with balanced mode
await StartScanAsync(BluetoothScanMode.Balanced);
// Subscribe to device discovery
_scanner.DeviceListChanged += OnDeviceListChanged;
try
{
// After 10 seconds without finding device, switch to low latency
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
if (!_deviceFound)
{
_logger.LogInformation("No device found, switching to LowLatency mode");
await UpdateScanModeAsync(BluetoothScanMode.LowLatency);
}
// After 30 seconds total, give up or switch to low power
await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken);
if (!_deviceFound)
{
_logger.LogInformation("Extended search, switching to LowPower mode");
await UpdateScanModeAsync(BluetoothScanMode.LowPower);
}
}
finally
{
_scanner.DeviceListChanged -= OnDeviceListChanged;
}
}
private void OnDeviceListChanged(object? sender, DeviceListChangedEventArgs e)
{
if (e.AddedDevices.Any(d => d.Name?.Contains("MyDevice") == true))
{
_deviceFound = true;
}
}
private async Task StartScanAsync(BluetoothScanMode mode)
{
await _scanner.StartScanningAsync(new ScanningOptions { ScanMode = mode });
}
private async Task UpdateScanModeAsync(BluetoothScanMode newMode)
{
if (_scanner.IsRunning)
{
await _scanner.UpdateScannerOptionsAsync(
new ScanningOptions { ScanMode = newMode });
}
}
}
Scan Duration Management
Continuous scanning drains battery quickly. Always limit scan duration.
Time-Limited Scanning
public class TimeLimitedScanner
{
private readonly IBluetoothScanner _scanner;
public async Task<IBluetoothRemoteDevice?> ScanWithTimeoutAsync(
string targetDeviceName,
TimeSpan scanDuration,
CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(scanDuration);
try
{
await _scanner.StartScanningAsync(
new ScanningOptions
{
ScanMode = BluetoothScanMode.Balanced,
IgnoreNamelessAdvertisements = true
},
cancellationToken: cts.Token);
// Wait for device or timeout
var device = await WaitForDeviceAsync(
targetDeviceName,
cts.Token);
return device;
}
catch (OperationCanceledException)
{
_logger.LogInformation("Scan timeout after {Duration}", scanDuration);
return null;
}
finally
{
// Always stop scanning to save battery
await _scanner.StopScanningAsync();
}
}
private async Task<IBluetoothRemoteDevice?> WaitForDeviceAsync(
string deviceName,
CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<IBluetoothRemoteDevice>();
void OnDeviceListChanged(object? sender, DeviceListChangedEventArgs e)
{
var device = e.AddedDevices.FirstOrDefault(d => d.Name == deviceName);
if (device != null)
{
tcs.TrySetResult(device);
}
}
_scanner.DeviceListChanged += OnDeviceListChanged;
try
{
using var registration = cancellationToken.Register(() =>
tcs.TrySetCanceled(cancellationToken));
// Check if device already in list
var existing = _scanner.Devices.FirstOrDefault(d => d.Name == deviceName);
if (existing != null)
return existing;
return await tcs.Task;
}
finally
{
_scanner.DeviceListChanged -= OnDeviceListChanged;
}
}
}
Stop Scanning Immediately After Finding Device
public class EfficientScanner
{
private readonly IBluetoothScanner _scanner;
public async Task<IBluetoothRemoteDevice> FindDeviceQuicklyAsync(
Func<IBluetoothRemoteDevice, bool> deviceMatcher,
CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<IBluetoothRemoteDevice>();
void OnDeviceListChanged(object? sender, DeviceListChangedEventArgs e)
{
var matchedDevice = e.AddedDevices.FirstOrDefault(deviceMatcher);
if (matchedDevice != null)
{
tcs.TrySetResult(matchedDevice);
}
}
_scanner.DeviceListChanged += OnDeviceListChanged;
try
{
await _scanner.StartScanningAsync(
new ScanningOptions { ScanMode = BluetoothScanMode.Balanced },
cancellationToken: cancellationToken);
using var registration = cancellationToken.Register(() =>
tcs.TrySetCanceled(cancellationToken));
var device = await tcs.Task;
// CRITICAL: Stop scanning immediately to save battery
await _scanner.StopScanningAsync();
return device;
}
finally
{
_scanner.DeviceListChanged -= OnDeviceListChanged;
}
}
}
Periodic Scanning Pattern
public class PeriodicScanner
{
private readonly IBluetoothScanner _scanner;
private CancellationTokenSource? _scanCts;
// Scan for 10 seconds every 60 seconds
private readonly TimeSpan _scanDuration = TimeSpan.FromSeconds(10);
private readonly TimeSpan _scanInterval = TimeSpan.FromSeconds(60);
public async Task StartPeriodicScanningAsync(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Starting scan cycle");
// Scan for limited duration
_scanCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_scanCts.CancelAfter(_scanDuration);
await _scanner.StartScanningAsync(
new ScanningOptions
{
ScanMode = BluetoothScanMode.LowPower, // Battery friendly
IgnoreDuplicateAdvertisements = true
},
cancellationToken: _scanCts.Token);
// Wait for scan to complete or timeout
await Task.Delay(_scanDuration, cancellationToken);
await _scanner.StopScanningAsync();
_logger.LogInformation("Scan cycle complete, sleeping for {Interval}", _scanInterval);
// Sleep until next scan cycle
await Task.Delay(_scanInterval - _scanDuration, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during periodic scan");
await Task.Delay(5000, cancellationToken); // Wait before retry
}
}
await _scanner.StopScanningIfNeededAsync();
}
public async Task StopPeriodicScanningAsync()
{
_scanCts?.Cancel();
await _scanner.StopScanningIfNeededAsync();
}
}
Background Scanning
Background scanning must be extremely battery-conscious.
Android Background Scanning
public class AndroidBackgroundScanner
{
private readonly IBluetoothScanner _scanner;
public async Task StartBackgroundScanAsync()
{
var options = new ScanningOptions
{
// Low power mode for background
ScanMode = BluetoothScanMode.LowPower,
// Or opportunistic for minimal battery impact
// ScanMode = BluetoothScanMode.Opportunistic,
// Request background location if needed (Android 10-11)
RequireBackgroundLocation = true,
// Reduce callback frequency
IgnoreDuplicateAdvertisements = true,
// Only scan for specific devices
ServiceUuids = new[] { MyServiceUuid },
// Platform-specific Android options
Android = new AndroidScanningOptions
{
// Report devices after multiple matches (battery friendly)
MatchMode = MatchMode.Sticky,
// Wait for 3 matches before reporting
NumOfMatches = MatchNum.FewAdvertisements,
// Batch results (report every 5 seconds)
ReportDelay = TimeSpan.FromSeconds(5)
}
};
await _scanner.StartScanningAsync(options);
}
}
iOS Background Scanning
public class IosBackgroundScanner
{
private readonly IBluetoothScanner _scanner;
public async Task StartBackgroundScanAsync()
{
// iOS automatically handles background scanning efficiently
// when you specify service UUIDs
var options = new ScanningOptions
{
// IMPORTANT: Must specify service UUIDs for background scanning on iOS
ServiceUuids = new[] { MyServiceUuid },
// iOS manages scan mode automatically in background
ScanMode = BluetoothScanMode.LowPower,
// Reduce duplicate callbacks
IgnoreDuplicateAdvertisements = true
};
await _scanner.StartScanningAsync(options);
}
}
Connection Priority Optimization
Connection priority affects latency and power consumption. Use appropriately for your use case.
Connection Priority Guidelines
public class ConnectionPriorityManager
{
public async Task ConfigureConnectionPriorityAsync(
IBluetoothRemoteDevice device,
ApplicationMode mode)
{
var priority = mode switch
{
// Real-time data (gaming, audio streaming)
ApplicationMode.RealTime => BluetoothConnectionPriority.High,
// Regular data transfer
ApplicationMode.Normal => BluetoothConnectionPriority.Balanced,
// Infrequent updates (temperature sensor, etc.)
ApplicationMode.PowerSaving => BluetoothConnectionPriority.LowPower,
_ => BluetoothConnectionPriority.Balanced
};
try
{
await device.RequestConnectionPriorityAsync(priority);
_logger.LogInformation(
"Set connection priority to {Priority} for mode {Mode}",
priority,
mode);
}
catch (Exception ex)
{
// iOS/Windows don't support this - it's Android-only
_logger.LogDebug(ex, "Connection priority not supported on this platform");
}
}
}
public enum ApplicationMode
{
RealTime,
Normal,
PowerSaving
}
Dynamic Priority Adjustment
public class DynamicPriorityManager
{
private readonly IBluetoothRemoteDevice _device;
private BluetoothConnectionPriority _currentPriority = BluetoothConnectionPriority.Balanced;
public async Task StartDataTransferAsync()
{
// Switch to high priority for data transfer
await SetPriorityAsync(BluetoothConnectionPriority.High);
}
public async Task FinishDataTransferAsync()
{
// Switch back to low power after transfer
await SetPriorityAsync(BluetoothConnectionPriority.LowPower);
}
private async Task SetPriorityAsync(BluetoothConnectionPriority priority)
{
if (_currentPriority == priority)
return;
try
{
await _device.RequestConnectionPriorityAsync(priority);
_currentPriority = priority;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to set connection priority");
}
}
}
MTU Optimization
Larger MTU means fewer packets, which reduces power consumption for large data transfers.
MTU Negotiation
public class MtuOptimizer
{
public async Task<int> NegotiateOptimalMtuAsync(
IBluetoothRemoteDevice device,
int dataSize)
{
// Default MTU is 23 bytes (20 bytes payload)
// Request larger MTU for better efficiency
int requestedMtu = dataSize switch
{
// Small transfers - default is fine
< 20 => 23,
// Medium transfers - request moderate MTU
< 200 => 200,
// Large transfers - request maximum MTU
_ => 517 // Maximum for BLE
};
try
{
var negotiatedMtu = await device.RequestMtuAsync(
requestedMtu,
timeout: TimeSpan.FromSeconds(5));
_logger.LogInformation(
"MTU negotiated: requested {Requested}, negotiated {Negotiated}",
requestedMtu,
negotiatedMtu);
return negotiatedMtu;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "MTU negotiation failed, using default");
return 23; // Default MTU
}
}
public async Task TransferLargeDataAsync(
IBluetoothRemoteDevice device,
IBluetoothRemoteCharacteristic characteristic,
byte[] data)
{
// Negotiate larger MTU before transfer
var mtu = await NegotiateOptimalMtuAsync(device, data.Length);
// Calculate optimal chunk size (MTU - 3 bytes for ATT header)
var chunkSize = mtu - 3;
// Transfer in chunks
for (int i = 0; i < data.Length; i += chunkSize)
{
var chunk = data.Skip(i).Take(chunkSize).ToArray();
await characteristic.WriteValueAsync(chunk);
}
_logger.LogInformation(
"Transferred {Size} bytes with MTU {Mtu}",
data.Length,
mtu);
}
}
Service Caching
Cache discovered services to avoid repeated discovery operations.
Service Cache Pattern
public class CachedServiceManager
{
private readonly IBluetoothRemoteDevice _device;
private bool _servicesExplored;
public async Task<IBluetoothRemoteService> GetServiceAsync(
Guid serviceUuid,
CancellationToken cancellationToken = default)
{
// Use cached services if already explored
if (!_servicesExplored)
{
await _device.ExploreServicesAsync(
ServiceExplorationOptions.WithCharacteristics,
cancellationToken: cancellationToken);
_servicesExplored = true;
}
// Get service from cache (no device query)
var service = _device.GetService(serviceUuid);
if (service == null)
{
// Force re-exploration if service not found
await _device.ExploreServicesAsync(
new ServiceExplorationOptions
{
UseCache = false, // Force refresh
IncludeCharacteristics = true
},
cancellationToken: cancellationToken);
service = _device.GetService(serviceUuid);
}
return service ?? throw new ServiceNotFoundException(serviceUuid);
}
public void InvalidateCache()
{
_servicesExplored = false;
}
}
Smart Cache Invalidation
public class SmartCacheManager
{
private readonly IBluetoothRemoteDevice _device;
private DateTime _lastExploration = DateTime.MinValue;
private readonly TimeSpan _cacheValidity = TimeSpan.FromMinutes(10);
public async Task<IReadOnlyList<IBluetoothRemoteService>> GetServicesAsync(
CancellationToken cancellationToken = default)
{
var cacheAge = DateTime.UtcNow - _lastExploration;
if (cacheAge > _cacheValidity)
{
// Cache expired - refresh
_logger.LogInformation("Service cache expired, refreshing...");
await _device.ExploreServicesAsync(
new ServiceExplorationOptions
{
UseCache = false,
IncludeCharacteristics = true
},
cancellationToken: cancellationToken);
_lastExploration = DateTime.UtcNow;
}
else
{
// Use cache
_logger.LogDebug("Using cached services (age: {Age})", cacheAge);
await _device.ExploreServicesAsync(
ServiceExplorationOptions.WithCharacteristics,
cancellationToken: cancellationToken);
}
return _device.Services;
}
}
Notification Management
Stop notifications when not needed to reduce power consumption.
Notification Lifecycle
public class NotificationManager
{
private readonly IBluetoothRemoteCharacteristic _characteristic;
public async Task StartMonitoringAsync()
{
if (!_characteristic.IsListening)
{
_characteristic.ValueUpdated += OnValueUpdated;
await _characteristic.StartListeningAsync();
_logger.LogInformation("Started notifications");
}
}
public async Task StopMonitoringAsync()
{
if (_characteristic.IsListening)
{
await _characteristic.StopListeningAsync();
_characteristic.ValueUpdated -= OnValueUpdated;
_logger.LogInformation("Stopped notifications");
}
}
private void OnValueUpdated(object? sender, CharacteristicValueUpdatedEventArgs e)
{
// Process notification
}
}
Conditional Notifications
public class ConditionalNotificationManager
{
private readonly IBluetoothRemoteCharacteristic _characteristic;
private bool _isAppInForeground;
public async Task OnAppStateChangedAsync(bool isInForeground)
{
_isAppInForeground = isInForeground;
if (isInForeground)
{
// App in foreground - enable notifications
await EnableNotificationsAsync();
}
else
{
// App in background - disable to save battery
await DisableNotificationsAsync();
}
}
private async Task EnableNotificationsAsync()
{
if (!_characteristic.IsListening)
{
_characteristic.ValueUpdated += OnValueUpdated;
await _characteristic.StartListeningAsync();
_logger.LogInformation("Enabled notifications (foreground)");
}
}
private async Task DisableNotificationsAsync()
{
if (_characteristic.IsListening)
{
await _characteristic.StopListeningAsync();
_characteristic.ValueUpdated -= OnValueUpdated;
_logger.LogInformation("Disabled notifications (background)");
}
}
private void OnValueUpdated(object? sender, CharacteristicValueUpdatedEventArgs e)
{
// Process notification
}
}
Platform-Specific Optimizations
Android Optimizations
public class AndroidBatteryOptimizations
{
public ScanningOptions GetOptimizedScanOptions(bool isBackgroundScan)
{
return new ScanningOptions
{
ScanMode = isBackgroundScan
? BluetoothScanMode.Opportunistic // Minimal battery impact
: BluetoothScanMode.LowPower,
IgnoreDuplicateAdvertisements = true,
Android = new AndroidScanningOptions
{
// Batch results for battery savings
ReportDelay = isBackgroundScan
? TimeSpan.FromSeconds(10)
: TimeSpan.Zero,
// Use sticky matching for background
MatchMode = isBackgroundScan
? MatchMode.Sticky
: MatchMode.Aggressive,
// Require multiple advertisements before reporting
NumOfMatches = isBackgroundScan
? MatchNum.FewAdvertisements
: MatchNum.OneAdvertisement,
// Use LE 1M PHY for better range and lower power
Phy = PhyType.Le1M
}
};
}
public ConnectionOptions GetOptimizedConnectionOptions(bool isLongLived)
{
return new ConnectionOptions
{
Android = new AndroidConnectionOptions
{
// Long-lived connections benefit from autoConnect
AutoConnect = isLongLived,
// Use low power priority for long-lived connections
ConnectionPriority = isLongLived
? BluetoothConnectionPriority.LowPower
: BluetoothConnectionPriority.Balanced,
// LE transport for BLE devices
TransportType = BluetoothTransportType.Le
}
};
}
}
iOS/macOS Optimizations
public class AppleBatteryOptimizations
{
public ScanningOptions GetOptimizedScanOptions()
{
return new ScanningOptions
{
// iOS manages scan mode automatically, but still use LowPower as hint
ScanMode = BluetoothScanMode.LowPower,
// CRITICAL: Always specify service UUIDs for background scanning
ServiceUuids = new[] { MyServiceUuid },
// Reduce duplicate callbacks
IgnoreDuplicateAdvertisements = true
};
}
public ConnectionOptions GetOptimizedConnectionOptions(bool isBackgroundCapable)
{
return new ConnectionOptions
{
Apple = new AppleConnectionOptions
{
// Only enable background alerts if absolutely necessary
NotifyOnConnection = isBackgroundCapable,
NotifyOnDisconnection = isBackgroundCapable,
NotifyOnNotification = isBackgroundCapable
}
};
}
}
Battery Life Measurement
Monitoring Battery Impact
public class BatteryImpactMonitor
{
private DateTime _scanStartTime;
private TimeSpan _totalScanTime;
private int _connectionCount;
private int _notificationCount;
public void OnScanStarted()
{
_scanStartTime = DateTime.UtcNow;
}
public void OnScanStopped()
{
_totalScanTime += DateTime.UtcNow - _scanStartTime;
_logger.LogInformation("Total scan time: {Duration}", _totalScanTime);
}
public void OnDeviceConnected()
{
_connectionCount++;
_logger.LogInformation("Total connections: {Count}", _connectionCount);
}
public void OnNotificationReceived()
{
_notificationCount++;
if (_notificationCount % 100 == 0)
{
_logger.LogInformation(
"Notifications received: {Count}",
_notificationCount);
}
}
public BatteryImpactReport GetReport()
{
return new BatteryImpactReport
{
TotalScanTime = _totalScanTime,
ConnectionCount = _connectionCount,
NotificationCount = _notificationCount
};
}
}
public record BatteryImpactReport
{
public TimeSpan TotalScanTime { get; init; }
public int ConnectionCount { get; init; }
public int NotificationCount { get; init; }
}
Summary
Battery Optimization Checklist
- [ ] Use LowPower or Balanced scan modes (avoid LowLatency unless necessary)
- [ ] Stop scanning immediately after finding device
- [ ] Limit scan duration (use timeouts)
- [ ] Specify service UUIDs when possible (especially for iOS background)
- [ ] Use IgnoreDuplicateAdvertisements to reduce callbacks
- [ ] Set connection priority to LowPower for infrequent data updates
- [ ] Negotiate larger MTU for bulk data transfers
- [ ] Cache service discovery results (use
UseCache = true) - [ ] Stop notifications when not needed
- [ ] Use periodic scanning instead of continuous scanning
- [ ] Implement opportunistic scanning for background Android apps
- [ ] Monitor battery impact during development and testing
Power Consumption Hierarchy
Highest Power Consumption:
- Continuous scanning with LowLatency mode
- Multiple simultaneous connections with High priority
- Frequent notifications with small MTU
- Repeated service discovery
Lowest Power Consumption:
- Opportunistic or periodic scanning with LowPower mode
- Single connection with LowPower priority
- Infrequent notifications with large MTU
- Cached service discovery
Related Topics
- Connection Management - Optimize connection strategies
- Performance - Balance performance with battery life
- Error Handling - Handle errors efficiently