How Bitchat Android Handles Fragmentation of Large Messages: A Deep Dive into the Bluetooth Mesh Protocol

Bitchat Android splits oversized packets into smaller fragment packets using a three-component system—FragmentManager, FragmentPayload, and FragmentingPacketSender—that ensures compatibility with the iOS implementation of Bitchat's Bluetooth mesh protocol.

Large message fragmentation is essential for any mesh networking application constrained by MTU limits. In Bitchat Android, this capability is implemented through a unified pipeline shared across Wi-Fi Aware and Bluetooth transports. According to the permissionlesstech/bitchat-android source code, the system transparently handles packets exceeding 512 bytes while maintaining cross-platform parity with the iOS reference implementation.

Core Components of the Fragmentation System

The fragmentation architecture centers on three tightly integrated classes:

Component Role Source File
FragmentManager Generates fragment plans, stores incoming fragments, reassembles complete messages FragmentManager.kt
FragmentPayload Defines the 13-byte fragment header and handles payload encoding/decoding FragmentPayload.kt
FragmentingPacketSender Wraps transport sends, streams fragments with progress tracking FragmentingPacketSender.kt

These components work together to ensure fragmentation of large messages happens automatically whenever payload size exceeds protocol limits.

When Fragmentation Is Triggered

The decision to fragment occurs in FragmentManager.createFragments, located in [FragmentManager.kt](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt) lines 84-86:

if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
    return listOf(packet) // No fragmentation needed
}

The FRAGMENT_SIZE_THRESHOLD constant is defined in AppConstants.Fragmentation at approximately 512 bytes. This threshold accommodates the maximum transmission unit (MTU) for Bluetooth Low Energy while reserving headroom for protocol headers and padding.

How the Fragment Plan Gets Built

When fragmentation is required, FragmentManager constructs a deterministic splitting strategy through four sequential steps:

  1. Fragment ID generation — FragmentPayload.generateFragmentID() creates a random 8-byte identifier (lines 74-78 in FragmentPayload.kt)

  2. Dynamic fragment size calculation — The system computes available payload space after accounting for headers, route information, and fixed padding overhead (lines 106-108 in FragmentManager.kt)

  3. Payload chunking — The stride helper function (lines 24-31) slices the original payload into equal-sized chunks, each wrapped in a FragmentPayload with proper sequencing metadata (lines 33-41)

  4. Fragment packet assembly — Each FragmentPayload is encoded and placed into a new BitchatPacket with version-appropriate formatting (v1 or v2) and type MessageType.FRAGMENT (lines 44-54)

The resulting list of BitchatPacket objects represents the complete fragment sequence ready for transmission.

Sending Fragments with Progress Tracking

The [FragmentingPacketSender.kt](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/mesh/FragmentingPacketSender.kt) class handles the actual transmission orchestration. Its send method performs the following operations:

// Obtains fragment list from FragmentManager (lines 21-53)
val fragments = fragmentManager.createFragments(routed.packet, maxFragments)

// Starts progress tracking for multi-fragment transfers (line 67)
if (fragments.size > 1) {
    progressManager.start(description, fragments.size)
}

// Serial transmission with configurable inter-fragment delay (lines 70-99)
fragments.forEachIndexed { index, fragment ->
    delay(interFragmentDelayMs) // Default: 20ms
    val success = sendSingle(routed.copy(packet = fragment))
    if (!success) {
        progressManager.fail(description)
        return@send false
    }
    progressManager.progress(description, index + 1)
}

Key transmission behaviors include:

  • Serial sending — Fragments transmit sequentially, not in parallel, to prevent receiver buffer overflow
  • Configurable pacing — interFragmentDelayMs defaults to 20 milliseconds between fragments
  • Atomic failure — Any single fragment failure aborts the entire transfer
  • Progress callbacks — TransferProgressManager updates UI components with real-time completion status

Reassembly on the Receiving Side

Incoming fragment handling occurs in FragmentManager.handleFragment (lines 70-88). The reassembly process follows a strict state machine:

Validation and Storage

// Decode and validate fragment header (lines 71-86)
val fragmentPayload = FragmentPayload.decode(packet.payload) ?: return null
if (!fragmentPayload.isValid) return null

// Enforce resource limits (lines 91-48)
if (incomingFragments[fragmentId]?.size ?: 0 >= MAX_FRAGMENTS_PER_ID) {
    return null // Reject excessive fragments for this ID
}

Completion Detection and Reconstruction

When the stored fragment count equals fragmentPayload.total, reassembly triggers:

// Concatenate fragments in correct order (lines 62-68)
val orderedData = (0 until total).flatMap { index ->
    fragmentsMap[index] ?: return null
}.toByteArray()

// Decode original packet and clear state (lines 71-76)
val originalPacket = BitchatPacket.fromBinaryData(orderedData)
incomingFragments.remove(fragmentId)
fragmentMetadata.remove(fragmentId)

// Return with TTL=0 for fresh processing
return PacketWithTtl(originalPacket, ttl = 0)

Automated Cleanup

A background coroutine (startPeriodicCleanup) runs every 10 seconds to purge stale fragments older than 30 seconds, preventing memory exhaustion from incomplete transfers.

Integration with Transport Layers

The fragmentation of large messages operates transparently across both supported transports:

Wi-Fi Aware

WifiAwareMeshService instantiates FragmentingPacketSender at line 91, injecting the shared FragmentManager from MeshCore:

val fragmentingSender = FragmentingPacketSender(
    scope = coroutineScope,
    fragmentManager = meshCore.fragmentManager,
    logTag = "WifiAwareMesh"
)

Bluetooth

BluetoothPacketBroadcaster follows the identical pattern at line 127, ensuring BLE broadcasts benefit from identical fragmentation semantics.

This unified approach guarantees that iOS ↔ Android message compatibility is preserved regardless of underlying transport technology.

Direct API Usage Examples

Developers can interact with the fragmentation system programmatically:

Manual Fragment Generation

val fragmentManager = FragmentManager()
val fragments: List<BitchatPacket> = fragmentManager.createFragments(
    packet = oversizedPacket,
    maxFragments = 0xFFFF  // Allow full UInt16 range
)
// fragments ready for custom transport injection

Custom Sender Configuration

val sender = FragmentingPacketSender(
    scope = CoroutineScope(Dispatchers.IO),
    fragmentManager = meshCore.fragmentManager,
    logTag = "CustomModule"
)

sender.send(
    routed = myRoutedPacket,
    description = "Large file broadcast",
    sendSingle = { routedPacket ->
        myCustomTransport.send(routedPacket.packet.bytes)
    }
)

Summary

  • Threshold-based triggering — Fragmentation activates automatically when un-padded payload exceeds ~512 bytes
  • Three-layer architecture — FragmentManager plans and reassembles, FragmentPayload structures headers, FragmentingPacketSender orchestrates transmission
  • Cross-platform compatibility — Fragment format matches iOS Bitchat implementation exactly
  • Robust resource management — Per-ID limits, global byte caps, and periodic cleanup prevent DoS via fragment flooding
  • Transport-agnostic design — Wi-Fi Aware and Bluetooth layers share identical fragmentation pipelines through dependency injection

Frequently Asked Questions

What is the maximum message size Bitchat Android can fragment?

The system supports up to 65,535 fragments (0xFFFF) per message ID. With approximately 500 bytes of payload per fragment after headers, this yields a theoretical maximum around 32 MB. However, practical limits are lower due to memory constraints and the 30-second reassembly timeout.

How does Bitchat ensure fragments arrive in order?

Reassembly uses the index field in the 13-byte FragmentPayload header to place chunks correctly regardless of arrival order. The FragmentManager stores fragments in a ConcurrentHashMap indexed by (fragmentId, index) tuples.

What happens if a fragment is lost in transmission?

The FragmentingPacketSender detects send failures and aborts the entire transfer, marking it failed through TransferProgressManager. The receiving side's 30-second timeout eventually cleans up partial fragment sets. There is no automatic retransmission—higher layers must detect failure and retry the complete message.

Is the fragmentation protocol compatible with other mesh networks?

No. The 13-byte FragmentPayload header structure—including 8-byte random ID, 2-byte index, 2-byte total count, and 1-byte original type—is specific to Bitchat's protocol. While designed for iOS ↔ Android compatibility, it does not interoperate with standard Bluetooth mesh or Thread fragmentation schemes.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →