# How BitChat Fragment Recovery Works for Stalled BLE Re‑assemblies

> Learn how BitChat fragment recovery tackles stalled BLE reassemblies. Discover its timestamp scanning, missing ID encoding, and peer retransmission solicitation for seamless data transfer.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-23

---

**BitChat detects stalled broadcast fragment streams by scanning `BLEFragmentAssemblyBuffer` timestamps, encodes missing fragment IDs into a `REQUEST_SYNC` packet filter, and solicits retransmission from peers who match the 8‑byte stream identifiers.**

BitChat fragments large BLE payloads into smaller packets for broadcast transmission. When packet loss or sender pauses interrupt a stream, the **BitChat fragment recovery** mechanism automatically identifies the stall and coordinates peer‑assisted reconstruction. This article dissects the three‑stage recovery pipeline implemented in the `permissionlesstech/bitchat` repository.

## Stage 1: Detecting Stalled Broadcast Streams

Detection begins in [`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift), which maintains per‑stream metadata including `lastFragmentAt` timestamps. The method `stalledBroadcastFragmentIDs(stalledAfter:retryAfter:now:)` implements the stall detection logic in [[`BLEFragmentAssemblyBuffer.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEFragmentAssemblyBuffer.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEFragmentAssemblyBuffer.swift#L170-L199).

The scanner applies three filters to active assemblies:

- **Incomplete streams** – `fragments.count < total` indicates missing pieces
- **Stall threshold** – No new fragment received for `stalledAfter` seconds (configured via `TransportConfig.bleFragmentResyncStallSeconds`)
- **Rate limiting** – `lastResyncRequestAt` plus `retryAfter` interval prevents duplicate queries

The method returns up to `RequestSyncPacket.maxFragmentIdFilterCount` (60) fragment‑ID `Data` values, marking selected streams with a fresh `lastResyncRequestAt` timestamp.

## Stage 2: Sending Targeted REQUEST_SYNC Packets

During its periodic maintenance cycle, `BLEService.performCleanup` invokes the detection method and forwards results to the sync layer. In [[`BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift#L7143-L7151), the service passes stalled IDs to `GossipSyncManager.requestMissingFragments`.

The manager encodes the IDs using `RequestSyncPacket.encodeFragmentIdFilter` into the **0x06 fragment‑ID filter** TLV field. This creates a compact bit‑filter carried by a `REQUEST_SYNC` packet broadcast to every connected peer. The wire format respects a 1024‑byte decoder cap, ensuring the filter never exceeds protocol limits.

## Stage 3: Peer‑Side Filtering and Retransmission

When a peer receives the request, `GossipSyncManager.handleRequestSync` in [[`GossipSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GossipSyncManager.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift#L39-L50) decodes the filter via `RequestSyncPacket.decodeFragmentIdFilter` ([[`RequestSyncPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/RequestSyncPacket.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Models/RequestSyncPacket.swift#L37-L46)).

For each stored fragment packet, the handler checks whether the packet’s first 8 bytes (the stream ID) exists in the decoded filter set. Matching packets are retransmitted with two critical modifications:

- **TTL reset to 0** – Prevents further propagation
- **`isRSR` flag set to true** – Marks the packet as a **Requested Solicited Response**, distinguishing it from unsolicited broadcasts

This allows the original receiver to accept the missing fragments and resume assembly.

## Key Design Constraints

**Broadcast‑Only Eligibility**
Only **broadcast** fragments qualify for recovery. Directed fragments are stored locally for gossip synchronization; requesting them from peers would never succeed because they exist only on the original recipient device.

**Rate Limiting**
The `lastResyncRequestAt` timestamp combined with the `retryAfter` parameter prevents the network from flooding peers with repeated requests for the same stalled stream.

**Wire‑Size Limits**
The implementation caps fragment‑ID filters at 60 entries (`maxFragmentIdFilterCount`) to stay within the 1024‑byte decoder capacity, ensuring predictable memory usage during packet parsing.

**Fast‑Path Execution**
Detection runs on the BLE service cleanup thread, identifying stalled streams before the next generic GCS fragment round, which reduces recovery latency compared to waiting for periodic full synchronization.

## Code Implementation Example

The following Swift patterns illustrate the recovery pipeline:

```swift
// 1. Buffer incoming fragments
var buffer = BLEFragmentAssemblyBuffer()
let header = BLEFragmentHeader(packet: incomingPacket)!  
let result = buffer.append(header, maxInFlightAssemblies: 50)

// 2. Periodic cleanup and stall detection
let now = Date()
buffer.removeExpired(before: now.addingTimeInterval(-TransportConfig.bleFragmentLifetimeSeconds))

let stalledIDs = buffer.stalledBroadcastFragmentIDs(
    stalledAfter: TransportConfig.bleFragmentResyncStallSeconds,
    retryAfter:  TransportConfig.bleFragmentResyncRetrySeconds,
    now: now
)

// 3. Request missing fragments from peers
if !stalledIDs.isEmpty {
    gossipSyncManager.requestMissingFragments(fragmentIDs: stalledIDs)
}

```

When handling requests on the peer side:

```swift
// 4. Process REQUEST_SYNC and retransmit matches
if request.types?.contains(.fragment) == true {
    let wantedIDs = RequestSyncPacket.decodeFragmentIdFilter(request.fragmentIdFilter)
    
    for pkt in fragments.allPackets(isFresh: isPacketFresh) {
        guard wantedIDs?.contains(Data(pkt.payload.prefix(8))) ?? true else { continue }
        
        var reply = pkt
        reply.ttl = 0
        reply.isRSR = true  // Mark as solicited response
        delegate?.sendPacket(to: peerID, packet: reply)
    }
}

```

## Summary

- **Detection** relies on `BLEFragmentAssemblyBuffer.stalledBroadcastFragmentIDs` scanning incomplete broadcast streams for timestamp gaps exceeding the stall threshold.
- **Requesting** uses `GossipSyncManager.requestMissingFragments` to encode up to 60 fragment IDs into a `REQUEST_SYNC` packet’s 0x06 filter.
- **Retransmission** occurs when peers match the 8‑byte stream ID prefix against stored fragments, returning them with the `isRSR` flag set and TTL zeroed.
- **Constraints** include broadcast‑only scope, rate limiting via `lastResyncRequestAt`, and a 1024‑byte wire‑size cap on filters.

## Frequently Asked Questions

### Why does BitChat only recover broadcast fragments and not directed fragments?

Directed fragments are delivered to specific recipients and stored only on the target device, not gossiped to the broader network. Because peers do not possess directed fragments belonging to other nodes, requesting them would always fail; thus, the protocol restricts recovery to **broadcast fragments** which all peers cache and can resupply.

### How does the protocol prevent spamming peers with repeated recovery requests?

`BLEFragmentAssemblyBuffer` tracks `lastResyncRequestAt` timestamps for each stalled stream. The `stalledBroadcastFragmentIDs` method skips streams that have been queried within the `retryAfter` interval, enforcing a cooldown period that prevents request flooding while still allowing eventual retries if the stream remains incomplete.

### What is the maximum number of fragment IDs that can be requested in a single recovery packet?

A single `REQUEST_SYNC` packet can carry at most **60 fragment IDs**, defined by `RequestSyncPacket.maxFragmentIdFilterCount`. This limit ensures the encoded filter remains within the 1024‑byte decoder cap and prevents oversized packets that could congest the BLE link layer.

### How does a receiver distinguish between a solicited recovery response and a regular broadcast?

Peers set the **`isRSR` (Requested Solicited Response)** flag to `true` when retransmitting fragments in response to a `REQUEST_SYNC` filter match. The receiving node examines this boolean flag to identify the packet as a targeted recovery response rather than an unsolicited broadcast fragment, allowing proper routing to the assembly buffer.