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:

  1. Discarding self-originated fragments to prevent re-assembling data after app relaunch
  2. Tracking broadcast fragments via trackPacketSeen for gossip-sync rounds
  3. Forwarding headers to the assembly buffer
  4. 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:

  1. Packet Receipt: The BLE service delivers each BitchatPacket to BLEFragmentHandler.handle(_:from:).

  2. Header Extraction: The handler attempts BLEFragmentHeader(packet:) initialization. If the packet lacks fragment metadata, processing returns early.

  3. 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.

  4. Gossip Sync Tracking: Broadcast fragments are recorded via trackPacketSeen so that subsequent gossip-sync rounds can identify missing pieces.

  5. Buffer Appending: The handler calls environment.appendFragment(header), which invokes BLEFragmentAssemblyBuffer.append. This method creates a new assembly entry via startAssemblyIfNeeded (evicting the oldest if at capacity), checks the projected size against assemblyLimit(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.

  6. Logging: logFragmentAssemblyResult generates debug output for each state transition (started, stored, completed, oversized).

  7. Re-injection: Upon receiving a .complete result, the handler decodes the raw bytes using BinaryProtocol.decode, validates the inner sender via isAcceptedIngressPayload, zeros the TTL, and pushes the reassembled packet through processReassembledPacket.

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 BLEFragmentKey indexing.
  • 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_SYNC packets.

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:

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 →