# How BitChat Handles BLE Packet Fragmentation and Reassembly for Large Messages

> Learn how BitChat manages BLE packet fragmentation and reassembly for large messages. Discover its efficient buffer and reordering strategy for seamless communication.

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

---

**BitChat splits messages exceeding the BLE MTU (~512 bytes) into fragments with 8-byte IDs, stores them in a capped assembly buffer, and reassembles them in-order before re-injecting complete packets into the normal receive pipeline.**

When sending large data over Bluetooth Low Energy, developers hit the MTU ceiling quickly. The open-source BitChat protocol solves this with a purpose-built fragmentation and reassembly system implemented across three core Swift components. This article examines exactly how [`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift) and [`BLEFragmentHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentHandler.swift) work together to reconstruct multi-packet messages reliably.

## The Fragment Header Structure

Every fragment begins with a **13-byte minimum header** parsed by `BLEFragmentHeader` in [[`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift#L9-L53). The header extractor pulls:

- Sender ID (identifies the source peer)
- 8-byte fragment ID (unique per message)
- Fragment index and total count (for ordering)
- Original packet type (preserved for reassembly)
- Broadcast flag (triggers gossip sync tracking)
- Fragment payload

```swift
// Extract header from incoming BLE packet
if let header = BLEFragmentHeader(packet: incomingPacket) {
    // header.id, header.index, header.total, header.isBroadcast now available
}

```

The header validation enforces the minimum size constraint. Packets that fail this check are treated as non-fragments and bypass the assembly pipeline.

## The Assembly Buffer: Storage and Eviction

**`BLEFragmentAssemblyBuffer`** manages concurrent fragment collections using a composite key of `sender + fragmentID`. Located in [[`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift) (lines 56-135)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift#L56-L135), the buffer implements several protective mechanisms:

### Size Limits and Eviction

The buffer enforces **per-type size limits** via `assemblyLimit(for:)`. When `append` is called:

1. `startAssemblyIfNeeded` creates a new entry for unseen keys
2. If `maxInFlightAssemblies` is exceeded, the **oldest assembly is evicted**
3. Projected total size is checked against the type limit
4. Oversized assemblies return `.oversized` and are discarded

### The Three Append Results

```swift
let result = buffer.append(header, maxInFlightAssemblies: 8)

switch result {
case .stored(let hdr, let started):
    // Fragment cached, waiting for more pieces
case .complete(let hdr, let data, _):
    // All fragments received—data contains reassembled bytes
case .oversized(let hdr, let projected, let limit, _):
    // Assembly aborted, would exceed size limits
}

```

### Out-of-Order Handling

Fragments are stored in a dictionary keyed by index: `fragmentsByKey[key][index]`. This allows **arrival in any order**—the buffer only requires complete index coverage before returning `.complete`.

## Fragment Handler: Orchestration and Re-Injection

**`BLEFragmentHandler`** in [[`BLEFragmentHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentHandler.swift) (lines 24-76)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentHandler.swift#L24-L76) serves as the entry point for all incoming `BitchatPacket` instances. Its responsibilities include:

### Self-Fragment Suppression

When the sender matches the local peer ID, fragments are tracked for gossip sync but **not assembled**. This prevents a device from inadvertently reassembling its own data after app relaunch.

### Broadcast Tracking

Broadcast fragments trigger `trackPacketSeen` calls. This enables **gossip sync recovery**—later rounds can detect missing fragments and request them specifically.

### Reassembly Completion Flow

When the buffer returns `.complete`:

```swift
// Inside BLEFragmentHandler.handle(_:from:)
case .complete(let hdr, let reassembledData, _):
    // 1. Decode the raw bytes back into a packet
    let packet = try BinaryProtocol.decode(reassembledData)
    
    // 2. Validate the inner sender
    guard isAcceptedIngressPayload(packet) else { return }
    
    // 3. Zero the TTL for fresh propagation
    packet.ttl = 0
    
    // 4. Re-inject into normal receive pipeline
    processReassembledPacket(packet)

```

This **zero-TTL reset** ensures reassembled packets get full propagation lifetimes rather than inheriting degraded TTL from their fragment journey.

## Stalled Broadcast Recovery

Broadcast fragments carry **time-to-complete risk**—peers may drop offline mid-transfer. The buffer detects stalled assemblies through timestamp tracking:

```swift
let stalledIDs = buffer.stalledBroadcastFragmentIDs(
    stalledAfter: 5,   // No progress for 5 seconds
    retryAfter: 10     // Wait 10 seconds between re-requests
)

// Encode stalledIDs into REQUEST_SYNC to recover missing fragments

```

Each new fragment index refreshes the "last fragment" timestamp. Assemblies with stale timestamps are flagged for **selective re-requesting** rather than blind retransmission.

## Complete Fragment Processing Example

```swift
import Foundation

// Configure buffer with conservative limits
var buffer = BLEFragmentAssemblyBuffer()
let maxConcurrent = 8
let sizeLimit: UInt32 = 1024 * 1024  // 1MB per message type

func handleBLEPacket(_ packet: BitchatPacket, from sender: PeerID) {
    // Skip non-fragments immediately
    guard packet.isFragment else {
        processDirectPacket(packet)
        return
    }
    
    // Extract and validate header
    guard let header = BLEFragmentHeader(packet: packet) else {
        logInvalidFragment(packet)
        return
    }
    
    // Route through buffer
    let result = buffer.append(header, maxInFlightAssemblies: maxConcurrent)
    
    logFragmentAssemblyResult(result, header: header)
    
    switch result {
    case .complete(let hdr, let data, _):
        do {
            let reassembled = try BinaryProtocol.decode(data)
            validateAndProcess(reassembled, originalHeader: hdr)
        } catch {
            logDecodeFailure(error, fragmentID: hdr.idLogString)
        }
        
    case .oversized(let hdr, let projected, let limit, _):
        reportOversizedDrop(sender: hdr.sender, 
                           fragmentID: hdr.id,
                           projected: projected,
                           limit: limit)
        
    case .stored:
        break  // Normal fragmentation in progress
    }
}

// Periodic stalled broadcast check
Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
    let stalled = buffer.stalledBroadcastFragmentIDs(
        stalledAfter: 5,
        retryAfter: 10
    )
    if !stalled.isEmpty {
        broadcastSyncRequest(fragmentIDs: stalled)
    }
}

```

## Summary

BitChat's BLE packet fragmentation and reassembly system handles large message transport through:

- **Structured fragment headers** with 8-byte unique IDs and explicit indexing
- **Capped assembly buffer** with LRU eviction and per-type size limits
- **Out-of-order tolerance** via indexed fragment storage
- **Self-fragment suppression** preventing local echo reassembly
- **Broadcast tracking** enabling gossip sync recovery
- **Stalled assembly detection** with selective re-request logic
- **TTL reset on completion** ensuring full propagation lifetimes

The implementation in [`permissionlesstech/bitchat`](https://github.com/permissionlesstech/bitchat) demonstrates production-ready Bluetooth Low Energy messaging without assuming reliable in-order delivery.

## Frequently Asked Questions

### What is the maximum message size BitChat can reassemble?

The buffer enforces **per-packet-type limits** via `assemblyLimit(for:)`. While the analysis shows 1MB typical configurations, specific limits depend on the `OriginalPacketType` of the fragmented message. Oversized assemblies return `.oversized` and are dropped rather than risking memory exhaustion.

### How does BitChat prevent memory exhaustion from partial fragments?

Three mechanisms protect against memory attacks: a **configurable cap on concurrent assemblies** (`maxInFlightAssemblies`), **per-type size projection checks** before accepting new fragments, and **LRU eviction of oldest assemblies** when capacity is exceeded. These are implemented in `BLEFragmentAssemblyBuffer.startAssemblyIfNeeded` and the size validation logic.

### Can fragments arrive out of order and still reassemble correctly?

Yes. The `fragmentsByKey` dictionary stores fragments keyed by their index within the message. The buffer tracks which indices have been received and only returns `.complete` when the received count equals `header.total`. No sequence number validation beyond index bounds checking is performed.

### What happens if a peer disappears mid-broadcast?

Broadcast fragments trigger timestamp tracking. The `stalledBroadcastFragmentIDs` method identifies assemblies with no new fragment arrivals within `stalledAfter` seconds. These IDs can be encoded into `REQUEST_SYNC` packets to request retransmission of specific missing fragments, scoped by the `retryAfter` cooldown to prevent sync spam.