# How BitChat Manages BLE Fragmentation and Reassembly for Messages Exceeding MTU

> Discover how BitChat handles BLE fragmentation and reassembly for messages over 512 bytes. Learn about its automatic packet splitting versioned fragments and receiver-side buffers.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-07-27

---

**BitChat automatically splits packets larger than the 512-byte BLE MTU into versioned fragments with unique headers and reassembles them on the receiver side using per-sender buffers that enforce size limits and handle stall recovery.**

BitChat, an open-source peer-to-peer messaging protocol from permissionlesstech/bitchat, transports messages over Bluetooth Low Energy (BLE) links constrained by a typical **maximum transmission unit (MTU)** of 512 bytes. When protocol messages exceed this limit, the system employs a sophisticated **BLE fragmentation and reassembly** strategy that preserves routing semantics, prevents self-reception loops, and supports legacy Android constraints. The implementation spans three distinct layers: fragment planning, binary encoding, and stateful reassembly.

## Fragment Planning and Chunk Sizing

When the BLE transport layer receives an oversized packet, it delegates fragmentation decisions to `BLEOutboundFragmentPlanner.makePlan` in **[BLEOutboundFragmentPlanner.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift)**.

The planner first serializes the entire packet using `request.packet.toBinaryData(padding:)`. It then applies a **sizing policy** to determine whether to use **fragment version 1** (for simple broadcasts without routing) or **version 2** (when the original packet carries a non-empty route). Version 2 fragments inherit the exact route field from the parent packet, requiring larger header overhead that is subtracted from the available MTU.

The final **chunk size** is calculated as the greater of 64 bytes (minimum threshold) and the user-requested or computed optimal size. The serialized data is then split using `stride` into discrete chunks, each wrapped in a fragment packet via `makeFragmentPacket`.

## Fragment Encoding and Binary Header Structure

Each fragment is encoded as a standard `BitchatPacket` with `type = MessageType.fragment.rawValue`, defined in **[MessageType.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/MessageType.swift)**. The binary payload prepends a compact header structure containing:

- **`fragmentID`**: 8-byte random identifier shared across all fragments of the same message
- **`index`**: 2-byte zero-based fragment number
- **`total`**: 2-byte total fragment count
- **`originalType`**: 1-byte original packet type (e.g., `MessageType.message`)
- **`fragmentData`**: Variable payload slice up to the calculated chunk size

Because fragments use the standard `BitchatPacket` container, they inherit generic binary encoding via `BinaryProtocol.encode` / `decode` and can be signed, encrypted, or routed exactly like non-fragmented traffic. As noted in **[BitchatPacket.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatPacket.swift)**, packets exceeding the BLE MTU are automatically funneled through this fragmentation pipeline.

## Fragment Reassembly and Reception Pipeline

Inbound fragment processing is orchestrated by `BLEFragmentHandler.handle` in **[BLEFragmentHandler.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentHandler.swift)**. The handler executes a strict validation and assembly sequence:

1. **Header Extraction**: `BLEFragmentHeader(packet:)` validates payload length and extracts the sender-derived 64-bit key, fragment ID, index, total count, and original type. Invalid fragments are immediately dropped.
2. **Self-Suppression**: Fragments originating from the local peer are ignored to prevent processing echoes of the device's own transmissions.
3. **Gossip Tracking**: Broadcast fragments are recorded via `trackPacketSeen` to prevent redundant retransmission during gossip synchronization.
4. **Assembly Buffering**: Valid fragments feed into `BLEFragmentAssemblyBuffer.append`, which maintains per-sender state in **[BLEFragmentAssemblyBuffer.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift)**.

The assembly buffer creates a new entry via `startAssemblyIfNeeded` when encountering the first fragment of a sequence. It enforces **size limits** via `assemblyLimit(for:)` based on the original packet type, discarding the entire stream if projected reconstruction would exceed safety thresholds. When `fragments.count == header.total`, the buffer concatenates payload pieces in index order, removes temporary state, and returns a `.complete` status.

Upon completion, the handler decodes the reassembled binary data back into a `BitchatPacket`, forces its `ttl` to 0 (indicating final hop arrival), and injects it into the normal receive pipeline via `processReassembledPacket`.

### Stall Detection and Recovery

The assembly buffer exposes `stalledBroadcastFragmentIDs` to identify incomplete broadcast streams that have not received new fragments within a configurable interval. This enables targeted `REQUEST_SYNC` packets to request missing pieces from peers, ensuring robust delivery even under lossy RF conditions.

## Version 1 vs. Version 2 Fragment Behavior

BitChat distinguishes between two fragmentation modes to preserve source-routing semantics:

- **Version 1**: Used for simple broadcasts without a route field. These fragments rely on standard flooding mechanics and carry minimal overhead.
- **Version 2**: Activated automatically when `packet.route` is non-empty. All fragments inherit the parent packet's route field, ensuring source-routed delivery semantics are maintained across the fragment stream. Without this version bump, fragments would fall back to flooding, violating the intended path constraints as documented in **[SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md)**.

The planner sets `fragmentVersion = 2` automatically when routing metadata is detected, ensuring the fragment header accommodates the additional routing bytes without exceeding MTU.

## Legacy Compatibility and Fragment Limits

For backwards compatibility with the legacy *private-media v1* transfer path on Android, the planner enforces a maximum fragment count of **256**. This limit is validated by `BLEOutboundFragmentPlanner.isPrivateMediaV1Compatible`, preventing transmission to older Android receivers that cannot handle larger fragment sequences.

## Fragmenting and Reassembling Messages in Practice

### Fragmenting a Large Packet for BLE Transmission

```swift
import BitFoundation

// Assume largePacket is a BitchatPacket > 512 bytes
let largePacket: BitchatPacket = // ... constructed elsewhere

let request = BLEOutboundFragmentTransferRequest(
    packet: largePacket,
    maxChunk: nil,                     // Allow planner to optimize size
    pad: true,
    directedPeer: nil,                 // Broadcast transmission
    // Additional fields omitted for brevity
)

let plan = BLEOutboundFragmentPlanner.makePlan(
    for: request,
    defaultChunkSize: 128,
    bleMaxMTU: 512
)

// plan?.fragmentPackets contains N fragments ready for transmission
// via BLEService.send(...)

```

*Source*: `BLEOutboundFragmentPlanner.makePlan` – **[BLEOutboundFragmentPlanner.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift)**

### Receiving and Reassembling Fragments

```swift
let handler = BLEFragmentHandler(
    environment: BLEFragmentHandlerEnvironment(
        localPeerID: { myPeerID },
        trackPacketSeen: { packet in 
            // Record for gossip deduplication
        },
        appendFragment: { header in 
            fragmentBuffer.append(
                header, 
                maxInFlightAssemblies: 64
            ) 
        },
        isAcceptedIngressPayload: { packet, sender in 
            // Validate sender permissions
            true 
        },
        processReassembledPacket: { packet, from in
            // Handle fully reconstructed packet
            receivePipeline.process(packet, from: from)
        }
    )
)

// Invoked by BLEService when raw packets arrive
handler.handle(incomingBitchatPacket, from: remotePeerID)

```

*Source*: `BLEFragmentHandler.handle` – **[BLEFragmentHandler.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentHandler.swift)**

## Summary

- **BLE fragmentation and reassembly** in BitChat is automatic for packets exceeding the 512-byte MTU, handled by `BLEOutboundFragmentPlanner` and `BLEFragmentHandler`.
- Fragment headers carry 8-byte IDs, indexing metadata, and original type information to enable ordered reconstruction.
- **Version 2 fragments** preserve source-routing paths across fragment boundaries, while Version 1 relies on flooding.
- The `BLEFragmentAssemblyBuffer` enforces per-type size limits and provides stall detection for broadcast recovery.
- Legacy Android compatibility caps fragment sequences at 256 pieces for private-media v1 transfers.

## Frequently Asked Questions

### What is the maximum message size BitChat can transmit over BLE?

While the BLE MTU is fixed at 512 bytes, BitChat supports arbitrarily large logical messages by splitting them into fragments. The practical limit is constrained by the `BLEFragmentAssemblyBuffer` size limits (which vary by packet type) and the 256-fragment cap for private-media v1 compatibility. Each fragment carries a 13-byte header overhead.

### How does BitChat prevent receiving its own fragmented messages?

The `BLEFragmentHandler` performs **self-fragment suppression** by comparing the fragment sender ID against the local peer ID. If they match, the fragment is discarded (though broadcast fragments are still tracked for gossip synchronization purposes to prevent retransmission loops).

### What happens if a fragment is lost during transmission?

Incomplete assemblies are tracked as **stalled** broadcasts if no new fragments arrive within a configurable timeout. The system exposes `stalledBroadcastFragmentIDs` to enable targeted `REQUEST_SYNC` packets that solicit missing fragments from peers, ensuring robust delivery without requiring full retransmission of the entire message.

### Why does BitChat use two fragment versions?

**Version 1** supports simple broadcast flooding without routing overhead. **Version 2** is required when the original packet contains source-routing metadata; it ensures all fragments carry the same route field, preventing the fallback to flooding that would occur if Version 1 were used for routed packets. This distinction is critical for maintaining path constraints across fragmented transmissions.