# How BitChat Handles BLE Message Fragmentation for Payloads Exceeding the MTU

> Discover how BitChat expertly manages BLE message fragmentation for large payloads. Learn about packet splitting, versioned fragments, and re-assembly ensuring reliable data transfer.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-08-09

---

**BitChat automatically splits packets larger than the 512-byte BLE MTU into versioned fragments with unique binary headers, transmits them as standard `BitchatPacket` messages, and re-assembles them on the receiver using per-sender buffers that enforce size limits, route preservation, and stall recovery.**

BitChat is an open-source, peer-to-peer messaging protocol built by permissionlesstech/bitchat that operates over Bluetooth Low Energy (BLE). Because BLE links typically enforce a maximum transmission unit (MTU) of 512 bytes, the protocol must fragment oversized payloads before transmission and reassemble them upon receipt. According to the source code in `bitchat/Services/BLE/`, this fragmentation layer operates transparently across three distinct stages: outbound planning, binary encoding, and inbound reassembly.

## Fragment Planning and Version Selection

When a packet is handed to the BLE transport, `BLEOutboundFragmentPlanner.makePlan` in **[BLEOutboundFragmentPlanner.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift)** evaluates whether fragmentation is required and computes the optimal transmission strategy.

The planner first serializes the entire packet via `request.packet.toBinaryData(padding:)` to measure its total size. It then applies a **sizing policy** to determine whether to use **fragment version 1** (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 to preserve source-routing semantics and avoid falling back to flooding, as documented in **[SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md)**.

The final **chunk size** is calculated as the greater of 64 bytes (the enforced minimum) or the user-requested size, subtracting header overhead from the 512-byte MTU. The binary data is then split using `stride(from:to:by:)` into individual chunks.

### Fragment Header Structure

For each chunk, `makeFragmentPacket` constructs a binary header containing:

- **`fragmentID`** – 8 bytes: A random identifier shared by all fragments of the same message
- **`index`** – 2 bytes: Zero-based fragment sequence number
- **`total`** – 2 bytes: Total fragment count
- **`originalType`** – 1 byte: The `MessageType` of the reassembled packet (e.g., `.message`)
- **`fragmentData`** – Variable: The payload slice (≤ chunk size)

The function returns a `BLEOutboundFragmentPlan` describing the packets, chosen version, chunk size, and inter-fragment spacing (which varies for directed vs. broadcast traffic).

## Fragment Encoding and Packet Wrapping

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)**. Because fragments use the generic packet structure, they benefit from standard binary encoding (`BinaryProtocol.encode`), encryption, and signing.

The `BitchatPacket` documentation explicitly notes this behavior:

> “Packets larger than BLE MTU (512 bytes) are automatically fragmented” – **[BitchatPacket.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatPacket.swift#L14-L15)**

For legacy **private-media v1** transfers, the planner enforces a hard limit of **256 fragments** (the maximum Android can accept) via `BLEOutboundFragmentPlanner.isPrivateMediaV1Compatible`.

## Inbound Reassembly and Buffer Management

When a BLE peripheral receives a fragment, `BLEService.handleFragment` delegates to **`BLEFragmentHandler.handle`** in **[BLEFragmentHandler.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentHandler.swift)**. The handler executes four critical steps:

1. **Header extraction** – `BLEFragmentHeader(packet:)` validates payload length and extracts the 64-bit sender key, fragment ID, index, total count, and original type. Invalid headers result in immediate drops.
2. **Self-fragment suppression** – Fragments originating from the local peer ID are discarded to prevent reprocessing, though broadcast fragments are still tracked for gossip-sync purposes.
3. **Assembly tracking** – `appendFragment` invokes **`BLEFragmentAssemblyBuffer.append`** in **[BLEFragmentAssemblyBuffer.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift)**. The buffer creates a per-sender entry on first receipt (`startAssemblyIfNeeded`), enforces size limits (`assemblyLimit(for:)`), and concatenates payloads in order.
4. **Re-injection** – When `fragments.count == header.total`, the buffer returns `.complete`. The handler decodes the binary blob back to a `BitchatPacket`, resets its `ttl` to 0, and injects it into the normal receive pipeline via `processReassembledPacket`.

### Stall Detection and Recovery

The assembly buffer tracks incomplete broadcast streams. Assemblies that have not received new fragments within a configurable interval are exposed via `stalledBroadcastFragmentIDs`, enabling the protocol to issue targeted `REQUEST_SYNC` packets to retrieve missing pieces.

## Version 1 vs. Version 2 Fragments

BitChat distinguishes between two fragmentation schemas to optimize routing efficiency:

- **Version 1** – Used for simple broadcasts without a route. The fragment header contains no routing metadata; fragments are forwarded by normal flooding.
- **Version 2** – Used when the original packet carries a non-empty `route`. All fragments inherit the same route field, preserving source-routing semantics and preventing fallback to flooding. The planner automatically sets `fragmentVersion = 2` when `packet.route` is non-empty.

As noted in the design documentation: “All fragments MUST be marked as Version 2 … otherwise they would fall back to flooding” – **[SOURCE_ROUTING.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md#when-a-large-source-routed-packet-exceeds-the-mtu-and-requires-fragmentation)**.

## Practical Implementation Examples

### Fragmenting a Large Packet for BLE Transmission

```swift
import BitFoundation

// Assume we have a BitchatPacket > 512 bytes
let largePacket: BitchatPacket = // ... built elsewhere

let request = BLEOutboundFragmentTransferRequest(
    packet: largePacket,
    maxChunk: nil,              // Allow planner to optimize
    pad: true,
    directedPeer: nil,          // Broadcast transmission
    // ... additional configuration
)

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

// plan?.fragmentPackets contains N fragments ready for transmission
// Source: BLEOutboundFragmentPlanner.makePlan – L26-L74

```

### Receiving and Reassembling Fragments

```swift
let handler = BLEFragmentHandler(
    environment: BLEFragmentHandlerEnvironment(
        localPeerID: { myPeerID },
        trackPacketSeen: { packet in /* gossip tracking */ },
        appendFragment: { header in 
            fragmentBuffer.append(header, maxInFlightAssemblies: 64) 
        },
        isAcceptedIngressPayload: { packet, sender in 
            /* validation logic */ 
        },
        processReassembledPacket: { packet, from in
            // Packet is now complete and injected into normal pipeline
            receivePipeline.process(packet, from: from)
        }
    )
)

// Called by BLEService when raw data arrives
handler.handle(incomingBitchatPacket, from: remotePeerID)
// Source: BLEFragmentHandler.handle – L34-L73

```

## Summary

- **Automatic splitting** – `BLEOutboundFragmentPlanner.makePlan` divides payloads exceeding the 512-byte BLE MTU into chunks of at least 64 bytes, selecting Version 1 or 2 based on routing requirements.
- **Binary headers** – Each fragment carries an 13-byte header (8-byte ID, 2-byte index, 2-byte total, 1-byte type) wrapped in a standard `BitchatPacket` with `MessageType.fragment`.
- **Per-sender assembly** – `BLEFragmentAssemblyBuffer` manages reassembly, enforces size limits per packet type, and drops incomplete streams that exceed thresholds.
- **Route preservation** – Version 2 fragments inherit the original packet's route to maintain source-routing integrity and avoid flooding.
- **Stall recovery** – The buffer exposes stalled broadcast fragments, enabling targeted synchronization requests for missing pieces.

## Frequently Asked Questions

### What is the maximum payload size BitChat can fragment?

BitChat targets a 512-byte BLE MTU, subtracting header overhead to determine chunk size. While there is no explicit upper bound on the original payload, the **private-media v1** compatibility layer caps fragments at **256 total pieces** to accommodate Android receiver limits. Standard packets are constrained only by available memory and the per-type size limits enforced by `BLEFragmentAssemblyBuffer.assemblyLimit(for:)`.

### How does BitChat prevent routing loops with fragmented packets?

When a source-routed packet requires fragmentation, the planner automatically selects **Version 2**, ensuring all fragments inherit the original packet's `route` field. This preserves the exact source path and prevents the routing engine from falling back to broadcast flooding, which could create loops or redundant traffic.

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

If a fragment is lost, the `BLEFragmentAssemblyBuffer` will never reach the expected fragment count (`header.total`), leaving the assembly incomplete. For broadcast traffic, these stalled assemblies are tracked and exposed via `stalledBroadcastFragmentIDs`. The protocol can then issue `REQUEST_SYNC` messages to specifically request the missing fragment indices from peers.

### How does BitChat handle self-fragments during relay?

The `BLEFragmentHandler` compares the fragment's sender ID against the local peer ID. If they match, the fragment is **discarded** (self-fragment suppression) to prevent the node from reprocessing its own traffic. However, broadcast fragments are still tracked via `trackPacketSeen` to maintain gossip synchronization state across the mesh.