# BLE Fragment Transfer Scheduling Mechanisms in BitChat: A Deep Dive into the Mesh Stack Architecture

> Explore BitChat's BLE fragment transfer scheduling. Discover how its mesh stack architecture ensures deterministic, deadlock-free message fragmentation and transfer using efficient queue management and radio back-pressure.

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

---

**TL;DR:** BitChat uses a layered mesh architecture where the **Engine** fragments large messages on a serial queue and enqueues them into **bleQueue‑confined buffers**, while the **BLELinkLayer** drains those buffers respecting radio back‑pressure, MTU limits, and BLE write‑ack timing to achieve deterministic, deadlock‑free fragment transfer.

BitChat's Bluetooth Low Energy (BLE) transport implements a sophisticated **BLE fragment transfer scheduling** system designed for reliable mesh communication. The architecture cleanly separates link‑layer radio management from higher‑level message processing, using strict queue confinement and back‑pressure mechanisms to prevent deadlocks and buffer overflows. This design is documented in the project's BLE architecture specification and implemented across the Swift source codebase.

## The Three‑Layer Scheduling Architecture

BitChat's fragment transfer relies on three interacting components, each with distinct responsibilities and queue confinement guarantees.

### BLELinkLayer: Radio Duty‑Cycle and Buffer Ownership

The **BLELinkLayer** owns all CoreBluetooth interaction, including advertising, scanning, connection scheduling, and MTU negotiation. Crucially, it maintains the back‑pressure buffers that mediate between the engine and the radio:

- `pendingPeripheralWrites` — buffers for outbound writes
- `pendingNotifications` — buffers for notification‑style transfers
- `pendingWriteBuffers` — additional write buffering

These buffers are **bleQueue‑confined**, meaning only code running on `bleQueue` may access them. According to the architecture documentation, "bleQueue owns the link buffers" including `pendingPeripheralWrites` and `pendingNotifications`【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L95-L99】.

The link layer also enforces the **radio duty‑cycle**, ensuring that scanning and advertising windows respect power constraints while maintaining mesh connectivity.

### Engine: Serial Queue Fragmentation

The **Engine** runs on a single serial queue (`mesh.message`) and owns all message‑level processing:

- Wire codec (serialization/deserialization)
- **Fragmentation** of large payloads
- Deduplication of received packets
- Peer registry state management

The architecture document explicitly lists "wire codec, **fragmentation**, dedup" as engine responsibilities【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L39-L41】. By performing fragmentation on the serial engine queue, BitChat guarantees that fragment creation and ordering are race‑free—no two messages interleave their fragments.

### The Scheduling Contract: Enqueue, Don't Block

The engine never writes directly to CoreBluetooth. Instead, it follows a strict **enqueue‑and‑return** pattern:

1. Slice the payload into MTU‑sized fragments
2. Pass each fragment to `BLELinkLayer.enqueueFragment()`
3. Return immediately, without waiting for radio transmission

This contract eliminates blocking across layers. The link layer asynchronously drains its buffers based on actual radio capacity, not engine demand.

## Strict Sync‑Edge Ordering: Preventing Deadlocks

BitChat enforces a **unidirectional synchronization order** that guarantees progress:

```

main / test threads → engine → bleQueue → (optional crypto queues)

```

As documented in the architecture spec, "No component may block waiting for a downstream component," which ensures that **fragment scheduling proceeds monotonically** without circular waits【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L71-L74】.

The `onEngine` helper enforces that only the engine may synchronously call into `bleQueue`, never the reverse. This prevents any scenario where the link layer could block waiting for engine processing【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L56-L63】.

## Fragment Dispatch Flow: Step by Step

The complete **BLE fragment transfer scheduling** sequence operates as follows:

| Step | Actor | Action |
|:---|:---|:---|
| 1. Fragment creation | Engine | Slices large message into MTU‑sized fragments on `mesh.message` queue |
| 2. Enqueue | Engine | Calls `BLELinkLayer.enqueueFragment()`, placing data into `pendingPeripheralWrites` or `pendingNotifications` |
| 3. Back‑pressure drain | BLELinkLayer | `drainWriteBuffersIfPossible()` removes and sends fragments only while `peripheralCanWrite()` returns true |
| 4. Completion | Engine | Marks transfer complete after final fragment acknowledgment; may trigger next queued message |

The link layer's drain method respects **peripheral‑advertised flow control**. It does not speculate about capacity—it waits for explicit write‑ready indications from CoreBluetooth.

## Code Implementation

Here's how the scheduling mechanism translates to Swift code, adapted from the BitChat source:

```swift
// MARK: - Engine side (mesh.message queue)
func sendLargeMessage(_ data: Data, to peer: BLEPeer) {
    // 1️⃣ Fragment payload according to negotiated MTU
    let fragments = BLEFragmenter.fragment(data, mtu: currentMTU)
    
    // 2️⃣ Enqueue each fragment for link-layer scheduling
    fragments.forEach { fragment in
        // Engine never blocks; returns immediately after enqueue
        bleLinkLayer.enqueueFragment(fragment, for: peer)
    }
}

// MARK: - BLELinkLayer side (bleQueue)
func enqueueFragment(_ fragment: Data, for peer: BLEPeer) {
    // Buffer is bleQueue-confined; no locking needed
    pendingPeripheralWrites.append((fragment, peer))
    drainWriteBuffersIfPossible()
}

// MARK: - Back-pressure aware draining
func drainWriteBuffersIfPossible() {
    while let (fragment, peer) = pendingPeripheralWrites.first,
          peripheralCanWrite(peer) {  // Respects BLE flow-control
        peripheralWrite(fragment, to: peer)
        pendingPeripheralWrites.removeFirst()
    }
    // If peripheralCanWrite() returns false, loop exits naturally
    // and awaits next CoreBluetooth callback
}

```

## Test Verification: Back‑Pressure Behavior

The test suite validates the scheduling contract under constrained conditions. In [`BLEServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEServiceTests.swift), the test **"fragment‑transfer respects back‑pressure"** creates a mock peripheral that accepts only a limited number of concurrent writes, then verifies that the engine correctly pauses until the peripheral signals readiness【/cache/repos/github.com/permissionlesstech/bitchat/main/bitchatTests/BLEServiceTests.swift#L120-L140】.

This test confirms that the **BLE fragment transfer scheduling** mechanism does not overflow peripheral buffers or drop fragments under radio congestion.

## Key Source Files

| File | Purpose |
|:---|:---|
| [`docs/BLE-ARCHITECTURE-V3.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/BLE-ARCHITECTURE-V3.md) | Complete design specification including scheduling contracts and queue confinement rules【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/BLE-ARCHITECTURE-V3.md#L31-L38】 |
| [`bitchatTests/BLEServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/BLEServiceTests.swift) | Unit tests for fragment creation, enqueuing, and back‑pressure handling【/cache/repos/github.com/permissionlesstech/bitchat/main/bitchatTests/BLEServiceTests.swift#L120-L140】 |
| `bitchat/BLEService+LinkLayerCentralRole.swift` | Link‑layer central implementation with buffer management |
| `bitchat/BLEService+LinkLayerPeripheralRole.swift` | Link‑layer peripheral implementation |

## Summary

- **Layer separation**: Engine handles fragmentation on `mesh.message`; BLELinkLayer handles radio scheduling on `bleQueue`
- **Buffer confinement**: `pendingPeripheralWrites`, `pendingNotifications`, and `pendingWriteBuffers` are bleQueue‑confined, eliminating race conditions
- **Enqueue‑and‑return**: Engine never blocks on radio I/O; fragments are asynchronously drained based on actual peripheral capacity
- **Sync‑edge ordering**: Unidirectional `engine → bleQueue` call pattern prevents deadlocks and guarantees monotonic progress
- **Back‑pressure respect**: Link layer drains buffers only when `peripheralCanWrite()` permits, matching fragment scheduling to BLE radio reality

## Frequently Asked Questions

### How does BitChat prevent fragment loss during BLE congestion?

The **BLELinkLayer** maintains bleQueue‑confined buffers (`pendingPeripheralWrites`, `pendingNotifications`) that absorb fragments from the engine. The drain loop only executes writes while `peripheralCanWrite()` returns true, automatically pausing when the peripheral reports congestion. This back‑pressure mechanism ensures fragments remain queued in memory rather than being dropped at the radio interface.

### Why does BitChat use a serial queue for fragmentation instead of concurrent queues?

Fragmentation runs on the single `mesh.message` queue to guarantee **deterministic fragment ordering** and eliminate race conditions. Since the engine also handles deduplication and peer registry updates, serial execution ensures that a message's fragments are created, enqueued, and tracked as an atomic logical unit without interleaving fragments from other messages.

### What happens if a peripheral never acknowledges a fragment write?

The architecture's sync‑edge ordering prevents the engine from blocking on acknowledgment. The link layer owns timeout and retry logic within `bleQueue`—if a peripheral stalls, the buffer holds pending fragments while the link layer manages reconnection or error propagation via `LinkEvent`s. The engine continues processing other messages without deadlock.

### Can the engine overflow the link layer's buffers?

No. The enqueue operation is non‑blocking and returns immediately, but the buffers themselves are bounded by available memory. The engine's rate of fragment production is naturally throttled by the serial queue's processing speed, while the link layer's drain loop ensures fragments exit as radio capacity permits. Unbounded growth would indicate a bug (peripheral never reading) rather than a scheduling flaw.