# How BitChat Reassembles Fragmented Large Messages over BLE: A Deep Dive into the Fragment Assembly Pipeline

> Discover how BitChat reassembles fragmented large messages over BLE. Learn about its header parsing, buffer management, and re-injection pipeline for efficient data transfer.

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

---

**BitChat splits payloads exceeding the BLE MTU (~512 bytes) into fragment packets and reassembles them using a three-stage pipeline involving header parsing, bounded in-flight buffer management, and orchestrated re-injection.**

BLE's Maximum Transmission Unit (MTU) limits single-packet payloads to approximately 512 bytes. When BitChat messages exceed this threshold, the protocol transparently fragments them at the sender and reassembles them at the receiver. The reassembly logic lives in [`bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift) and [`BLEFragmentHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentHandler.swift), implementing defensive bounds checking, stalled-broadcast recovery, and protection against self-reassembly attacks.

## Core Components of the Fragment Reassembly System

### BLEFragmentHeader: Parsing Incoming Fragments

The `BLEFragmentHeader` struct, defined at lines 9-53 of [`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift), extracts structured metadata from raw `BitchatPacket` instances. It validates minimum packet size (≥13 bytes) and parses:

- **Sender ID**: The originating peer's identifier
- **Fragment ID**: An 8-byte unique identifier for this fragmented message
- **Fragment index** and **total count**: Position within the sequence
- **Original packet type**: The underlying message type once reassembled
- **Broadcast flag**: Whether this fragment belongs to a broadcast message

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

```

### BLEFragmentAssemblyBuffer: Bounded In-Flight Storage

The `BLEFragmentAssemblyBuffer` class (lines 56-135) manages partial assemblies indexed by `BLEFragmentKey` (sender + fragment ID). It enforces three critical safety limits:

| Limit | Purpose | Trigger |
|:---|:---|:---|
| `maxInFlightAssemblies` | Memory pressure protection | Eviction of oldest assembly |
| Per-type size limit (`assemblyLimit(for:)`) | Prevent oversized allocations | `.oversized` result, assembly dropped |
| `stalledAfter` / `retryAfter` | Detect incomplete broadcasts | `REQUEST_SYNC` generation for recovery |

The `append(_:)` method returns three possible outcomes:

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

switch result {
case .stored(let hdr, let started):
    // Fragment buffered, `started` true if new assembly created
case .complete(let hdr, let data, _):
    // All fragments received; `data` contains concatenated payload
case .oversized(let hdr, let projected, let limit, _):
    // Assembly exceeded size limit and was discarded
}

```

Fragment storage uses a dictionary-of-arrays structure: `fragmentsByKey[key][index]`. When a new index arrives, the "last fragment" timestamp refreshes, enabling stalled-broadcast detection.

### BLEFragmentHandler: Orchestration and Re-Injection

[`BLEFragmentHandler.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentHandler.swift) (lines 24-76) serves as the entry point for all inbound fragments. Its `handle(_:from:)` method implements several defensive checks before appending to the buffer:

1. **Self-fragment suppression**: If `sender == localPeerID`, the fragment is tracked (for broadcast gossip sync) but not assembled—preventing a device from reassembling its own data after app relaunch
2. **Gossip sync tracking**: Broadcast fragments register via `trackPacketSeen` for later `REQUEST_SYNC` rounds
3. **Result logging**: `logFragmentAssemblyResult` emits debug telemetry for each stage

Upon `.complete`, the handler:

```swift
// Decode reassembled bytes using BinaryProtocol
let packet = try BinaryProtocol.decode(data)

// Validate inner sender against accept list
guard isAcceptedIngressPayload(packet, from: header.sender) else { return }

// Zero TTL and re-inject into normal receive pipeline
processReassembledPacket(packet.withZeroTTL())

```

## The Complete Reassembly Flow

The following sequence diagram summarizes the end-to-end flow as implemented in `permissionlesstech/bitchat`:

1. **Packet receipt** — BLE service delivers `BitchatPacket` to `BLEFragmentHandler.handle(_:from:)`
2. **Header validation** — `BLEFragmentHeader(packet:)` parses metadata; non-fragments pass through unmodified
3. **Self-origin check** — Local fragments tracked but not assembled
4. **Broadcast tracking** — `trackPacketSeen` registers fragment for gossip sync
5. **Buffer append** — `BLEFragmentAssemblyBuffer.append` manages storage, eviction, and completion detection
6. **Completion handling** — Raw bytes decoded, validated, TTL zeroed, and re-injected via `processReassembledPacket`

## Stalled-Broadcast Recovery

Broadcast messages present unique challenges: without ACKs, fragments may be lost silently. The buffer's `stalledBroadcastFragmentIDs(stalledAfter:retryAfter:)` method identifies assemblies with no recent progress:

```swift
let stalledIDs = buffer.stalledBroadcastFragmentIDs(
    stalledAfter: 5,   // Seconds since last new fragment
    retryAfter: 10     // Minimum seconds before re-requesting same ID
)
// stalledIDs contains big-endian fragment IDs for REQUEST_SYNC construction

```

This enables proactive recovery via gossip protocol rounds rather than waiting for timeouts.

## Summary

- **BLEFragmentHeader** parses 13+ byte packets into structured fragment metadata in [`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift)
- **BLEFragmentAssemblyBuffer** provides bounded storage with eviction, size limits, and stalled-broadcast detection
- **BLEFragmentHandler** orchestrates inbound fragments, suppresses self-reassembly, validates completed packets, and re-injects them into the standard receive pipeline
- The pipeline protects against memory exhaustion via `maxInFlightAssemblies` and per-type size limits
- Broadcast-specific recovery uses timestamp tracking to generate targeted `REQUEST_SYNC` packets

## Frequently Asked Questions

### What is the minimum valid size for a BitChat fragment packet?

A fragment packet must be at least **13 bytes** to contain a valid `BLEFragmentHeader`. The `BLEFragmentHeader(packet:)` initializer returns `nil` for undersized packets, causing `BLEFragmentHandler` to pass them through unmodified.

### How does BitChat prevent memory exhaustion from incomplete fragment assemblies?

`BLEFragmentAssemblyBuffer` enforces `maxInFlightAssemblies` (configurable, typically 8). When this cap is reached, the **oldest assembly is evicted** before creating a new one. Additionally, per-message-type size limits (`assemblyLimit(for:)`) trigger immediate eviction of oversized assemblies with a `.oversized` return value.

### Why doesn't BitChat reassemble fragments from itself?

Self-originated fragments are detected when `sender == localPeerID`. While still **tracked for gossip sync** (to prevent rebroadcast loops), they are **not appended to the assembly buffer**. This defends against a relaunched app attempting to reassemble its own previously-transmitted fragments.

### How are out-of-order fragments handled during reassembly?

Fragments are stored in `fragmentsByKey[key][index]` using their declared index position. The buffer **does not require sequential arrival**; assembly completes when the number of stored fragments equals `header.total`, regardless of receive order.