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:
UInt16values 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 overflowbleFragmentSpacingMs/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
BLEOutboundFragmentPlannerautomatically 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.
BLEServiceschedules fragment transmission with configurable concurrency limits and spacing to prevent Bluetooth controller congestion.- The
BLEFragmentAssemblyBufferreassembles incoming fragments into completeBitchatPacketinstances, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →