How BitChat Handles Large Message Fragmentation Over BLE
BitChat splits payloads exceeding the BLE MTU into indexed fragments and reassembles them using a three-tier pipeline consisting of BLEFragmentHeader, BLEFragmentAssemblyBuffer, and BLEFragmentHandler, with built-in memory caps, oversized detection, and stalled-broadcast recovery.
When a message exceeds Bluetooth Low Energy's Maximum Transmission Unit (approximately 512 bytes), the BitChat protocol must fragment the payload across multiple packets and reliably reconstruct it on the receiving end. The permissionlesstech/bitchat repository implements a robust reassembly system that handles out-of-order delivery, memory constraints, and broadcast-specific synchronization. Understanding how this large message fragmentation over BLE works reveals the architectural decisions behind reliable mesh communication.
The Architecture Behind BLE Fragment Reassembly
The reassembly pipeline relies on three tightly integrated components that parse, buffer, and orchestrate fragments.
BLEFragmentHeader
The BLEFragmentHeader struct, defined in bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift (lines 9-53), parses metadata from an incoming BitchatPacket. It validates that the raw packet contains at least 13 bytes, then extracts:
- An 8-byte fragment ID uniquely identifying the message stream
- The fragment index and total fragment count
- The original sender ID and packet type
- A flag indicating whether this is a broadcast fragment
If the packet fails minimum size validation or does not represent a fragment, the header initialization returns nil, allowing the handler to process it as a standard packet.
BLEFragmentAssemblyBuffer
The BLEFragmentAssemblyBuffer class (lines 56-135 in BLEFragmentAssemblyBuffer.swift) manages transient storage for in-progress assemblies. It indexes fragments using a composite BLEFragmentKey combining the sender ID and fragment ID.
Key safety mechanisms include:
maxInFlightAssemblies: A configurable cap on concurrent assemblies. When the limit is exceeded, the buffer evicts the oldest entry (LRU policy).assemblyLimit(for:): Per-packet-type size limits that trigger eviction if the projected total exceeds the threshold.- Timestamp tracking: Each assembly records the "last fragment" arrival time to enable stalled-broadcast detection.
The buffer's append(_:) method returns one of three enumeration cases:
.stored: The fragment was accepted and the assembly remains incomplete..complete: All fragments arrived; the method returns the concatenated data..oversized: The projected total exceeds the safety limit; the assembly is discarded.
BLEFragmentHandler
The BLEFragmentHandler in bitchat/Services/BLE/BLEFragmentHandler.swift (lines 24-76) serves as the entry point for all BLE traffic. Its handle(_:from:) method orchestrates the reassembly flow by:
- Discarding self-originated fragments to prevent re-assembling data after app relaunch
- Tracking broadcast fragments via
trackPacketSeenfor gossip-sync rounds - Forwarding headers to the assembly buffer
- Re-injecting completed packets into the standard receive pipeline after zeroing the TTL
The Fragment Reassembly Flow
When a large message arrives, BitChat processes it through a deterministic seven-stage pipeline:
-
Packet Receipt: The BLE service delivers each
BitchatPackettoBLEFragmentHandler.handle(_:from:). -
Header Extraction: The handler attempts
BLEFragmentHeader(packet:)initialization. If the packet lacks fragment metadata, processing returns early. -
Self-Fragment Suppression: If the sender ID matches the local peer ID, the fragment is tracked (if broadcast) but not assembled, preventing redundant processing of the device's own transmissions.
-
Gossip Sync Tracking: Broadcast fragments are recorded via
trackPacketSeenso that subsequent gossip-sync rounds can identify missing pieces. -
Buffer Appending: The handler calls
environment.appendFragment(header), which invokesBLEFragmentAssemblyBuffer.append. This method creates a new assembly entry viastartAssemblyIfNeeded(evicting the oldest if at capacity), checks the projected size againstassemblyLimit(for:), and stores the fragment data at the specified index. When the stored count equals the advertised total, it concatenates fragments in order and returns.complete. -
Logging:
logFragmentAssemblyResultgenerates debug output for each state transition (started, stored, completed, oversized). -
Re-injection: Upon receiving a
.completeresult, the handler decodes the raw bytes usingBinaryProtocol.decode, validates the inner sender viaisAcceptedIngressPayload, zeros the TTL, and pushes the reassembled packet throughprocessReassembledPacket.
Memory Management and Safety Limits
BitChat enforces strict memory boundaries during reassembly to prevent denial-of-service via maliciously large or numerous fragment streams.
When BLEFragmentAssemblyBuffer.append detects a new assembly key, it checks the current count against maxInFlightAssemblies. If the cap is reached, it evicts the oldest incomplete assembly before allocating storage. Each assembly’s projected total size is validated against assemblyLimit(for:) based on the original packet type. Assemblies exceeding this limit are immediately purged and reported as .oversized, freeing the associated memory.
Recovering Stalled Broadcasts
Broadcast packets present unique challenges in mesh networks where fragments may be lost or delayed. BitChat implements stalled-broadcast detection via stalledBroadcastFragmentIDs in the assembly buffer.
Called periodically, this method identifies assemblies that have received no new fragments for stalledAfter seconds. It respects a retryAfter window to avoid spamming recovery requests. The method returns fragment IDs (encoded big-endian) that should be targeted by a REQUEST_SYNC packet, enabling the mesh to recover missing fragments without requiring full retransmission of the original large message.
Implementation Example
The following Swift demonstrates fragment reception and reassembly:
// 1️⃣ Create a fragment header from a raw BLE packet
if let header = BLEFragmentHeader(packet: incomingPacket) {
// 2️⃣ Append it to the assembly buffer (max 8 concurrent assemblies)
let result = buffer.append(header, maxInFlightAssemblies: 8)
// 3️⃣ React to the result
switch result {
case .stored(let hdr, let started):
print("Stored fragment \(hdr.index+1)/\(hdr.total), started: \(started)")
case .complete(let hdr, let data, _):
print("Reassembled packet of \(data.count) bytes for id \(hdr.idLogString)")
// Decode & process the full packet…
case .oversized(let hdr, let projected, let limit, _):
print("Dropping oversized assembly (projected \(projected) > limit \(limit))")
}
}
To detect and recover stalled broadcasts:
// 4️⃣ Detect stalled broadcast fragments (called periodically)
let stalledIDs = buffer.stalledBroadcastFragmentIDs(
stalledAfter: 5, // seconds without new fragment
retryAfter: 10) // seconds before re‑requesting
// Encode these IDs into a REQUEST_SYNC packet to recover missing fragments.
Summary
- BLEFragmentHeader parses fragment metadata from raw packets in
BLEFragmentAssemblyBuffer.swift. - BLEFragmentAssemblyBuffer provides LRU eviction, per-type size limits, and stalled-broadcast tracking via
BLEFragmentKeyindexing. - BLEFragmentHandler orchestrates the flow, filters self-fragments, and re-injects completed packets after TTL zeroing.
- The three-tier architecture handles large message fragmentation over BLE while preventing memory exhaustion through configurable caps and oversized detection.
- Stalled broadcast recovery uses timestamp tracking to request missing fragments via
REQUEST_SYNCpackets.
Frequently Asked Questions
What triggers message fragmentation in BitChat?
BitChat automatically fragments messages when the payload exceeds the BLE MTU of approximately 512 bytes. The protocol splits the data into indexed chunks, each wrapped in a BitchatPacket with a BLEFragmentHeader containing sequence numbers and total fragment count.
How does BitChat prevent memory exhaustion during fragment reassembly?
The BLEFragmentAssemblyBuffer enforces a configurable maxInFlightAssemblies limit, evicting the oldest incomplete assembly when the cap is reached. Additionally, assemblyLimit(for:) checks projected total sizes against per-type safety limits, immediately discarding any assembly that would exceed its allocated memory threshold.
How does BitChat handle fragments received out of order?
Fragments are stored in a dictionary keyed by their index within the assembly buffer. The buffer waits until the count of stored fragments equals the advertised total before concatenating them in sequential order. This design tolerates arbitrary arrival sequences as long as all fragments arrive within the stalled-broadcast timeout window.
What is the purpose of zeroing the TTL on reassembled packets?
When BLEFragmentHandler completes reassembly, it zeros the Time-To-Live (TTL) before re-injecting the packet into the receive pipeline. This prevents the reassembled message from being re-transmitted indefinitely through the mesh, ensuring that only the original sender's TTL policy governs propagation scope.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →