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

> Discover BitChat's BLE mesh deduplication mechanism. Learn how LRU caching and jitter prevent duplicate message floods, improving network efficiency.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-19

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) (line 421) explicitly references this as "Relay jitter scheduling to reduce redundant floods," while [`BLEEngineScheduler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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

```swift
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

```swift
// 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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/TransportConfig.swift), defaulting to 25 milliseconds. [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) (line 421) references this value when scheduling relay operations, and [`BLEEngineScheduler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEEngineScheduler.swift) executes the actual delayed dispatch using `DispatchQueue.asyncAfter`.