Deduplication Mechanism in BitChat's BLE Mesh: How Jitter Prevents Duplicate Floods

BitChat's BLE mesh employs an LRU-based MessageDeduplicator to instantly drop duplicate packets before processing, while randomized relay jitter (up to 25ms) desynchronizes forwards and maximizes the deduplication cache hit rate.

BitChat (permissionlesstech/bitchat) implements a packet-radio stack over BLE where every inbound packet passes through a strict deduplication layer prior to attribution or relay. Understanding the deduplication mechanism used in BitChat's BLE mesh reveals how the protocol maintains efficiency across multi-hop relay networks without succumbing to broadcast storms.

The MessageDeduplicator Cache

The core deduplication logic resides in bitchat/Utils/MessageDeduplicator.swift. When a BLE packet arrives, the engine constructs a unique identifier by concatenating the sender ID, timestamp, and packet type, then queries MessageDeduplicator.isDuplicate(id).

If the ID exists in the cache, the packet drops immediately. This prevents the "duplicate flood" scenario where multiple relays forward identical packets across the mesh. Fresh packets insert into the cache and proceed to fragmentation reassembly and attribution.

Bounded Memory Management

The deduplicator guarantees predictable resource consumption through dual eviction policies defined in TransportConfig:

  • Time-based eviction: Entries expire after TransportConfig.messageDedupMaxAgeSeconds
  • Count-based eviction: LRU eviction triggers when the cache reaches TransportConfig.messageDedupMaxCount

These bounds cover the typical latency window of a BLE mesh hop while preventing unbounded memory growth in constrained device environments.

Jitter's Role in Duplicate Prevention

Jitter works synchronously with deduplication to reduce redundant transmissions. When the engine decides to relay a packet, it schedules the forward after a random delay rather than broadcasting instantaneously.

Desynchronizing Relay Transmissions

The jitter delay is bounded by TransportConfig.bleFragmentRelayMaxDelayMs (default 25ms). This randomization ensures simultaneous relays on different hops rarely emit packets at identical moments. The implementation comment in BLEService.swift (line 421) explicitly references this as "Relay jitter scheduling to reduce redundant floods," while BLEEngineScheduler.swift handles the actual asynchronous dispatch.

Maximizing Dedup Cache Efficiency

The randomized delay creates a critical temporal window that allows the MessageDeduplicator cache to register the packet ID before secondary copies arrive. When subsequent relays transmit the same packet within the jitter window, the receiver's cache already contains the identifier, causing isDuplicate() to return true and drop the redundant transmission before it triggers another relay cycle.

Implementation Code Examples

The following patterns demonstrate how BitChat's BLE layer integrates deduplication and jitter in production code.

Checking for Duplicates on Ingest

let deduplicator = MessageDeduplicator() // Uses default TransportConfig
let packetID = "\(senderID.hexEncodedString())-\(packet.timestamp)-\(packet.type)"

// In the inbound pipeline (BLEService.swift)
if deduplicator.isDuplicate(packetID) {
    // Duplicate detected → immediate return skips processing
    return
}

// Unique packet proceeds to fragment assembly and attribution

Scheduling Relays with Jitter

// Excerpt from BLEService.swift relay logic
let jitterMs = Int.random(in: 0..<TransportConfig.bleFragmentRelayMaxDelayMs)
let jitterDelay = DispatchTimeInterval.milliseconds(jitterMs)

engineQueue.asyncAfter(deadline: .now() + jitterDelay) {
    // Actual BLE write occurs after randomized delay
    self.sendPacket(packet, to: nextHop)
}

Configuring Jitter and Cache Bounds

// In TransportConfig.swift
static let bleFragmentRelayMaxDelayMs: Int = 25   // Upper jitter bound for fragment relays
static let messageDedupMaxAgeSeconds: TimeInterval = 30
static let messageDedupMaxCount: Int = 1000

Summary

BitChat's BLE mesh combines two defensive mechanisms to prevent duplicate floods:

  • LRU-based deduplication: The MessageDeduplicator class in bitchat/Utils/MessageDeduplicator.swift maintains a bounded cache of seen packet IDs, instantly dropping duplicates identified by sender-timestamp-type strings.
  • Randomized relay jitter: A configurable random delay up to bleFragmentRelayMaxDelayMs (25ms) desynchronizes forwards, allowing the dedup cache to populate before redundant copies arrive.

Together, these mechanisms ensure efficient multi-hop propagation with minimal redundant BLE transmissions and predictable memory usage.

Frequently Asked Questions

How does BitChat generate the unique packet identifier for deduplication?

BitChat constructs the deduplication key by concatenating the sender's hex-encoded ID, packet timestamp, and packet type into a single string. This composite identifier passes to MessageDeduplicator.isDuplicate() in bitchat/Utils/MessageDeduplicator.swift, which checks against the LRU cache of recently seen packets.

What happens if the deduplication cache is full when a new packet arrives?

When the cache reaches TransportConfig.messageDedupMaxCount, the LRU (Least Recently Used) eviction policy removes the oldest entries to make room. Additionally, entries older than messageDedupMaxAgeSeconds expire automatically, ensuring the cache always stays within bounded memory limits while covering the typical mesh hop latency window.

Why does BitChat use jitter instead of forwarding packets immediately?

Immediate forwarding risks synchronized retransmissions where multiple relays broadcast identical packets simultaneously, causing each other to receive and re-forward duplicates exponentially. The randomized jitter (up to 25ms) implemented in BLEEngineScheduler.swift breaks this synchronization, allowing the MessageDeduplicator cache time to register the original packet ID and reject subsequent duplicates as they arrive.

Where is the jitter delay configured in the BitChat source code?

The upper bound for relay jitter is defined as bleFragmentRelayMaxDelayMs in bitchat/Services/TransportConfig.swift, defaulting to 25 milliseconds. BLEService.swift (line 421) references this value when scheduling relay operations, and BLEEngineScheduler.swift executes the actual delayed dispatch using DispatchQueue.asyncAfter.

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 →