# How Message Fragmentation Works for Large Payloads over Bluetooth LE MTU in Bitchat

> Learn how Bitchat handles large payloads over Bluetooth LE MTU using automatic message fragmentation and reassembly with BLEOutboundFragmentPlanner and BLEFragmentAssemblyBuffer.

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

---

**Bitchat automatically splits oversized `BitchatPacket` payloads into BLE-compliant fragments using `BLEOutboundFragmentPlanner` to calculate chunk sizes based on the 512-byte MTU limit, then reassembles them on the receiver via `BLEFragmentAssemblyBuffer`.**

Bitchat is a peer-to-peer messaging protocol developed by Permissionless Tech that transmits data over Bluetooth Low Energy (BLE). When **message fragmentation for large payloads over Bluetooth LE MTU** becomes necessary, the protocol slices packets into transport-safe chunks and reconstructs them transparently without requiring application-level changes.

## Calculating Fragment Size in BLEOutboundFragmentPlanner

The fragmentation process begins in [[`BLEOutboundFragmentPlanner.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEOutboundFragmentPlanner.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift). This class determines the optimal chunk size by accounting for the negotiated BLE MTU and protocol overhead.

### Accounting for Protocol Overhead

Before splitting data, the planner subtracts fixed overhead from the `bleMaxMTU` (typically **512 bytes**) to determine usable capacity:

```swift
// BLEOutboundFragmentPlanner.swift - sizingPolicy
let overhead = 16 + 8 + 8 + routeSize + 13 + 16
calculatedChunk = max(minimumChunkSize, bleMaxMTU - overhead)

```

The overhead accounts for authentication tags (16 bytes), the 8-byte fragment ID, index/total counters (8 bytes), variable routing information, and additional protocol headers. When a packet contains a route, the planner automatically selects **fragment version 2** to signal the extended header format.

### Enforcing Minimum and Maximum Limits

The planner enforces a `minimumChunkSize` of **64 bytes** to prevent inefficient micro-fragments. It also respects the `privateMediaV1MaxFragments` limit of **256 fragments**, rejecting packets that would exceed this threshold to maintain backward compatibility with Private Media v1 specifications.

## Building Fragment Packets

Once the `chunkSize` is established, `BLEOutboundFragmentPlanner` splits the serialized `BitchatPacket` using Swift’s `stride` function:

```swift
let chunks = stride(from: 0, to: fullData.count, by: sizing.chunkSize)
               .map { offset in
                   Data(fullData[offset..<min(offset + sizing.chunkSize,
                                            fullData.count)])
               }

```

For each chunk, the planner constructs a fragment packet containing:

- **Fragment ID**: An 8-byte random identifier shared across all fragments of the same message
- **Index/Total**: `UInt16` values indicating position and total fragment count
- **MessageType.fragment**: Distinguishes fragments from standard packets
- **Payload**: The original packet header plus the current data slice

## Transmitting Fragments via BLEService

The [`BLEService.sendFragmentedPacket`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) method (approximately line 6639) receives the fragment plan and delegates transmission to `BLEOutboundFragmentTransferScheduler`. This scheduler respects transport constraints by enforcing:

- **`bleMaxConcurrentTransfers`**: Limits simultaneous fragment writes to prevent queue overflow
- **`bleFragmentSpacingMs`** / **`bleFragmentSpacingDirectedMs`**: Configurable delays between fragments to avoid congestion

Each fragment is written to the BLE peripheral only when the scheduler confirms the transport can accept it, ensuring that **large payloads over Bluetooth LE MTU** never exceed the negotiated packet size.

## Receiving and Reassembling Fragments

On the receiving device, [[`BLEFragmentHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentHandler.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentHandler.swift) intercepts incoming fragments and forwards them to [[`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift).

### Assembly Buffer Management

The assembly buffer maintains an internal dictionary keyed by the 8-byte fragment ID. It stores each payload slice in a sequence buffer until the number of received fragments matches the expected `total` count. If fragments are missing or stalled, the buffer triggers resynchronization requests through the gossip-sync manager rather than waiting indefinitely.

### Completing Reassembly

When the final fragment arrives, `BLEFragmentAssemblyBuffer` concatenates all slices in index order, reconstructs the original `BitchatPacket`, and injects it into the standard packet processing pipeline. This reassembly is transparent to higher-level application logic.

## Code Examples

### Creating a Fragmented Transfer

```swift
import BitFoundation

// Prepare a packet larger than the 512-byte MTU
let largePacket = BitchatPacket(
    type: MessageType.chat.rawValue,
    senderID: myPeerID,
    recipientID: nil,
    timestamp: Date(),
    payload: largeData, // Data exceeding MTU
    signature: nil,
    ttl: 60,
    version: 1,
    route: nil,
    isRSR: false
)

// Configure the transfer request
let request = BLEOutboundFragmentTransferRequest(
    packet: largePacket,
    pad: false,
    maxChunk: nil,
    directedPeer: nil,
    reservedTransferId: nil
)

// Generate the fragmentation plan
guard let plan = BLEOutboundFragmentPlanner.makePlan(
    for: request,
    defaultChunkSize: TransportConfig.bleDefaultFragmentSize,
    bleMaxMTU: 512
) else {
    fatalError("Packet exceeds maximum fragment limit")
}

// Transmit each fragment
plan.fragmentPackets.forEach { fragment in
    BLEService.shared.sendFragmentedPacket(fragment, pad: false,
                                           maxChunk: nil,
                                           directedOnlyPeer: nil)
}

```

### Handling Incoming Fragments

```swift
// Inside BLEFragmentHandler.swift
func handleFragment(_ packet: BitchatPacket, from peerID: PeerID) {
    // Append to assembly buffer
    if let completePacket = BLEFragmentAssemblyBuffer.shared.append(packet) {
        // Reassembly complete - process the full message
        processIncomingPacket(completePacket, from: peerID)
    }
}

```

## Key Source Files

| File | Purpose | GitHub Link |
|------|---------|-------------|
| [`BLEOutboundFragmentPlanner.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEOutboundFragmentPlanner.swift) | Calculates chunk sizes, generates fragment packets, enforces 256-fragment limit | [View File](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEOutboundFragmentPlanner.swift) |
| [`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) | Entry point for `sendFragmentedPacket`, handles transfer scheduling and pacing | [View File](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift) |
| [`BLEFragmentHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentHandler.swift) | Receives inbound fragments and routes to assembly buffer | [View File](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentHandler.swift) |
| [`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift) | Reconstructs original packets from fragments, manages partial state and timeouts | [View File](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift) |
| [`BitchatPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatPacket.swift) | Defines the packet format including `MessageType.fragment` | [View File](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatPacket.swift) |

## Summary

- **`BLEOutboundFragmentPlanner`** automatically calculates fragment sizes by subtracting protocol overhead from the 512-byte BLE MTU, ensuring every chunk respects transport limits and enforcing a minimum 64-byte chunk size.
- Fragment packets carry unique 8-byte IDs with index/total metadata, enabling reliable reconstruction even when fragments arrive out of order or across different connection intervals.
- **`BLEService`** schedules fragment transmission with configurable concurrency limits and spacing to prevent Bluetooth controller congestion.
- The **`BLEFragmentAssemblyBuffer`** reassembles incoming fragments into complete `BitchatPacket` instances, detecting missing pieces and coordinating retransmission via the gossip-sync layer.
- The protocol enforces a hard limit of 256 fragments per packet for Private Media v1 compatibility.

## Frequently Asked Questions

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

If a fragment fails to arrive within the expected window, `BLEFragmentAssemblyBuffer` detects the missing index during reassembly. The buffer notifies the gossip-sync manager, which can request retransmission of the specific fragment from the sender. This selective recovery ensures reliable delivery without requiring the entire message to be resent.

### Why does Bitchat use fragment version 2 for routed packets?

Fragment version 2 indicates that additional routing overhead is present in the packet structure. When a `BitchatPacket` contains a non-nil route, `BLEOutboundFragmentPlanner` accounts for the extra bytes in the overhead calculation and sets the version to 2, allowing receiving peers to parse the extended header correctly while maintaining backward compatibility with version 1 fragments used for direct messaging.

### What is the maximum message size Bitchat can fragment?

The protocol limits messages to **256 fragments** (`privateMediaV1MaxFragments`). With a typical usable chunk size of approximately 450 bytes after accounting for overhead on a 512-byte MTU connection, this allows messages up to roughly **115 KB** to be transmitted while maintaining Private Media v1 compatibility.

### How does Bitchat prevent BLE congestion when sending large files?

The `BLEOutboundFragmentTransferScheduler` inside `BLEService` enforces `bleMaxConcurrentTransfers` to limit simultaneous operations and applies `bleFragmentSpacingMs` delays between individual fragment writes. This pacing prevents the Bluetooth controller queue from overflowing and maintains stable connections during large transfers.