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

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/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:

// 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:

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 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/bitchat/Services/BLE/BLEFragmentHandler.swift) intercepts incoming fragments and forwards them to [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

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

// 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 Calculates chunk sizes, generates fragment packets, enforces 256-fragment limit View File
BLEService.swift Entry point for sendFragmentedPacket, handles transfer scheduling and pacing View File
BLEFragmentHandler.swift Receives inbound fragments and routes to assembly buffer View File
BLEFragmentAssemblyBuffer.swift Reconstructs original packets from fragments, manages partial state and timeouts View File
BitchatPacket.swift Defines the packet format including MessageType.fragment View File

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.

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 →