BitChat BLE Mesh Battery Optimization: 7 Power-Saving Strategies in the Stack

BitChat minimizes battery consumption in its BLE mesh by coordinating radio operations through a serial engine queue, implementing flood controls with adaptive relay thresholds, and throttling announcements while pausing scans during heavy transmission loads.

BitChat is an open-source peer-to-peer messaging application built by permissionlesstech that creates resilient offline networks using Bluetooth Low Energy. Achieving efficient battery optimization in BitChat’s BLE mesh requires balancing reliable message propagation with minimal radio usage, which the codebase addresses through a strict separation between the link-layer and protocol engine. The architecture centralizes power-sensitive decisions within a serial dispatch queue, eliminating costly cross-thread coordination while enabling adaptive behaviors that scale radio duty cycles based on network conditions.

Serial Engine Queue Architecture

BitChat’s BLE stack divides responsibilities between the link-layer (BLELinkLayer), which handles raw radio I/O, and the serial engine queue, which owns all protocol state including throttling, fragment pacing, and relay probability calculations. This design ensures that power state changes—such as stopping scans or delaying retries—execute on a dedicated bleQueue without blocking the main thread or risking race conditions on the Bluetooth radio.

Flood Controls and Relay Thresholds

To prevent battery-draining broadcast storms, BLEService.swift (lines 200-202) implements flood controls that cap concurrent operations and adapt relay behavior based on network density. The implementation references centralized thresholds from TransportConfig.swift:

// Flood / battery controls (BLEService)
private let maxInFlightAssemblies = TransportConfig.bleMaxInFlightAssemblies   // caps concurrent fragment assemblies
private let highDegreeThreshold = TransportConfig.bleHighDegreeThreshold   // triggers adaptive TTL / probabilistic relays

When the mesh detects a high-degree node count exceeding highDegreeThreshold, the engine activates probabilistic relaying and reduces Time-To-Live (TTL) values, ensuring the radio does not waste energy rebroadcasting messages through already saturated pathways.

Scanning Pause During Fragment Transmission

Transmitting large payloads requires fragmenting data across multiple BLE writes. BitChat optimizes battery during these operations by temporarily halting BLE scanning to reduce the radio’s duty cycle. Found in BLEService.swift (lines 26-33), the logic calculates transmission duration and schedules a scan resumption after the expected write completion:

// Pause scanning while sending a long fragment train
if plan.shouldPauseScanning {
    bleQueue.async { [weak self] in
        guard let self = self, let c = self.centralManager, c.state == .poweredOn else { return }
        if c.isScanning { c.stopScan() }
        let totalFragments = plan.totalFragments
        let expectedMs = min(TransportConfig.bleExpectedWriteMaxMs,
                            totalFragments * TransportConfig.bleExpectedWritePerFragmentMs)
        self.bleQueue.asyncAfter(deadline: .now() + .milliseconds(expectedMs)) {
            self.radio.startScanning()
        }
    }
}

Adaptive Transmission Controls

Beyond raw radio management, BitChat implements intelligent scheduling layers that prevent unnecessary transmissions and respect device power states.

Announce Throttling

The BLEAnnounceThrottle.swift module enforces strict rate limits on public announce packets to prevent rapid broadcast bursts. Its shouldSend(force:now:) method distinguishes between normal and forced announcements, applying stricter intervals for forced broadcasts to conserve battery:

// Announce throttling (BLEAnnounceThrottle)
func shouldSend(force: Bool, now: Date) -> Bool {
    lock.withLock {
        let minInterval = force ? forcedMinimumInterval : normalMinimumInterval
        guard now.timeIntervalSince(lastSent) >= minInterval else { return false }
        lastSent = now
        return true
    }
}

Duty-Cycled Maintenance

The BLEMaintenancePolicy.swift file implements a maintenance loop that only emits announce packets every bleAnnounceIntervalSeconds (defaulting to 4 seconds). When the device maintains active connections, the policy extends intervals to reduce radio wake cycles:

// Maintenance policy – only announce after the configured interval
let elapsedSinceLastAnnounce = Date().timeIntervalSince(lastAnnounce)
guard elapsedSinceLastAnnounce >= TransportConfig.bleAnnounceIntervalSeconds else { return }

Configuration and Back-Pressure Management

Centralized configuration and thread-safe logging prevent CPU wake-ups and redundant processing that indirectly impact battery life.

Transport-Level Configuration

TransportConfig.swift defines constants governing BLE behavior, including default fragment sizes, maximum MTU, announce minimum intervals, and the high-degree threshold that drives adaptive relay logic. Centralizing these values allows the engine to calculate transmission costs and backoff periods without hardcoded magic numbers.

Queue-Based Back-Pressure

To avoid main thread contention, diagnostic counters such as notificationBackpressureLogCount update exclusively on the bleQueue rather than synchronizing across dispatch queues. As implemented in BLEService.swift (lines 14-16), this pattern reduces context switches and CPU overhead during high-throughput mesh operations.

Adaptive Battery Modes

According to the top-level documentation in README.md, BitChat supports adaptive battery modes that toggle between aggressive and conservative scanning behaviors based on application state (foreground vs. background), complementing the code-level optimizations with high-level power policies.

Summary

BitChat’s BLE mesh battery optimization relies on a self-regulating architecture that minimizes radio activation and thread coordination overhead:

  • Serial engine queue centralizes protocol state on a dedicated dispatch queue, preventing cross-thread synchronization costs.
  • Flood controls in BLEService.swift use maxInFlightAssemblies and highDegreeThreshold to limit concurrent processing and adaptive relaying.
  • Scan pausing halts BLE discovery during fragmented transmissions, calculating exact millisecond delays before resuming.
  • Announce throttling via BLEAnnounceThrottle enforces minimum intervals between broadcasts with stricter limits for forced announcements.
  • Duty-cycled maintenance in BLEMaintenancePolicy respects configurable intervals and extends them when connected.
  • Centralized constants in TransportConfig enable the engine to predict transmission costs and schedule efficient retries.
  • Queue-local logging keeps diagnostic updates off the main thread, reducing CPU wake-ups during mesh activity.

Frequently Asked Questions

How does BitChat prevent battery drain during large message transfers?

BitChat pauses BLE scanning while transmitting fragmented payloads, as implemented in BLEService.swift. The engine calculates the expected transmission duration based on fragment count and per-fragment write times, then schedules scanning to resume asynchronously on the bleQueue after completion, ensuring the radio does not simultaneously scan and transmit.

What triggers adaptive relay behavior in the BLE mesh?

When the number of visible peers exceeds the highDegreeThreshold defined in TransportConfig.swift, the mesh activates probabilistic relaying and reduces TTL values. This prevents high-density networks from wasting battery on redundant rebroadcasts while maintaining message reachability.

How does BitChat limit unnecessary broadcast traffic?

The BLEAnnounceThrottle class enforces minimum intervals between public announce packets, with distinct thresholds for normal and forced announcements. Additionally, BLEMaintenancePolicy skips announce cycles if the elapsed time since the last broadcast falls below bleAnnounceIntervalSeconds.

Why does BitChat use a serial queue for BLE operations?

The bleQueue serializes all protocol state changes, scanning controls, and fragment assembly decisions into a single execution context. This eliminates lock contention and race conditions that would otherwise require energy-intensive synchronization primitives, while ensuring the link-layer only receives commands from one thread at a time.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →