# How Bitchat Android Implements Store-and-Forward Messaging for Offline Peers

> Discover how Bitchat Android ensures message delivery to offline peers using its store-and-forward mechanism. Learn how packets are buffered and automatically forwarded upon reconnection for seamless communication.

- Repository: [permissionlesstech/bitchat-android](https://github.com/permissionlesstech/bitchat-android)
- Tags: internals
- Published: 2026-07-28

---

**Bitchat Android guarantees message delivery to offline peers by buffering packets in a dedicated `StoreForwardManager` that uses separate caches for favorite and regular contacts and forwards them automatically when the recipient reconnects.**

The [permissionlesstech/bitchat-android](https://github.com/permissionlesstech/bitchat-android) mesh messenger must operate across intermittent BLE and Wi-Fi Aware links where peers regularly disconnect. Its store-and-forward mechanism solves this by persisting direct messages in memory until the destination peer is reachable again. The subsystem is implemented in [`app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt) and is designed to be transport-agnostic.

## Core Components of the Store-and-Forward System

The `StoreForwardManager` class controls caching, deduplication, and retransmission without blocking UI threads.

### StoreForwardManager and Its Delegate

[`MeshCore.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/MeshCore.kt) instantiates the manager as a singleton property:

```kotlin
private val storeForwardManager = StoreForwardManager()

```

The manager receives runtime dependencies through the `StoreForwardManagerDelegate` interface declared at lines 41–46 and consumed at lines 312–316 of [`StoreForwardManager.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/StoreForwardManager.kt). The delegate exposes three methods:

- `isFavorite(peerID)` — Returns `true` if the recipient is a favorite contact.
- `isPeerOnline(peerID)` — Reports real-time connectivity status.
- `sendPacket(packet)` — Executes the actual transmission.

This design keeps `StoreForwardManager` agnostic to whether the underlying transport is BLE, Wi-Fi Aware, or Tor.

### Dual-Cache Architecture

The manager maintains two distinct in-memory stores to prioritize traffic and bound memory growth. Lines 35–38 of [`StoreForwardManager.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/StoreForwardManager.kt) define:

- **`messageCache`** — A synchronized list of `StoredMessage` objects for regular peers, capped by `MAX_CACHED_MESSAGES`.
- **`favoriteMessageQueue`** — A `ConcurrentHashMap` mapping peer IDs to dedicated queues for favorite contacts, capped by `MAX_CACHED_MESSAGES_FAVORITES`.
- **`deliveredMessages`** — A bookkeeping set that records message IDs already forwarded to prevent duplicates.

Separating favorites from regular peers ensures that high-priority contacts receive a larger buffer and faster delivery guarantees.

### Periodic Cleanup

A Kotlin coroutine launched via `startPeriodicCleanup` wakes every `CLEANUP_INTERVAL` (10 minutes) on an `IO` dispatcher. It invokes two routines:

- `cleanupMessageCache()` — Evicts non-favorite entries older than `MESSAGE_CACHE_TIMEOUT` (approximately 12 hours).
- `cleanupDeliveredMessages()` — Shrinks the `deliveredMessages` set to prevent indefinite growth.

Because cleanup runs on a background thread, the manager never stalls the main mesh event loop.

## Store-and-Forward Workflow for Offline Peers

The workflow is triggered by packet reception events inside [`MeshCore.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/MeshCore.kt) and completes when the target peer reconnects.

### Arrival and Filtering

When the mesh layer receives a packet, `MeshCore` calls:

```kotlin
val messageId = UUID.randomUUID().toString()
storeForwardManager.cacheMessage(packet, messageId)

```

Inside `cacheMessage` (lines 55–68), the manager first discards message types that never require caching, such as handshakes and announcements. It then drops broadcast packets and validates that the `recipientPeerID` is present and well-formed. Only directed messages survive this filter.

### Storage Routing

After validation, the manager queries its delegate via `isFavorite(recipientPeerID)` to choose a destination cache (lines 80–87):

- **Favorite peer** — The `StoredMessage` is appended to `favoriteMessageQueue[recipientPeerID]`. The queue is bounded by `MAX_CACHED_MESSAGES_FAVORITES`.
- **Regular peer** — The message is added to the global `messageCache`, bounded by `MAX_CACHED_MESSAGES`.

Both paths log the resulting cache size for observability (lines 89–115). If a cache is at capacity, the oldest entries are evicted before insertion.

### Reconnect and Forwarding

When the mesh service detects that a peer has come online, it invokes `sendCachedMessages(peerID)` (lines 21–78). This routine performs three steps:

1. **Gathering** — Collects all queued favorite messages for the peer and all regular `messageCache` entries whose `recipientID` matches. Already-delivered IDs present in `deliveredMessages` are excluded.
2. **Sorting** — The surviving packets are ordered by their original timestamps so that conversation history is relayed sequentially (lines 55–57).
3. **Transmission** — Each packet is emitted through `delegate?.sendPacket(storedMessage.packet)` with a 10 ms inter-message delay to avoid link saturation (lines 66–70). After successful dispatch, the message ID is recorded in `deliveredMessages` and the entry is removed from its cache (lines 62–74).

This sequence ensures eventual delivery while preserving causal order.

### Background Maintenance

Even if a peer never returns, the manager does not leak memory. The background coroutine repeatedly scrubs stale data, enforcing the 12-hour lifetime limit on regular cached messages and compacting the deduplication set.

## Integration with the Mesh Transport Layer

The delegate pattern makes `StoreForwardManager` reusable across transports. In [`BluetoothMeshService.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/BluetoothMeshService.kt), the delegate implements `isPeerOnline` by checking the current BLE GATT connection table, while [`WifiAwareMeshService.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/WifiAwareMeshService.kt) uses Wi-Fi Aware session state. Both services share the same manager instance wired through [`MeshCore.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/MeshCore.kt), proving that the store-and-forward logic is completely decoupled from radio-specific code.

The following snippet shows the typical API surface used by transport implementations:

```kotlin
// 1️⃣ Cache a new outgoing packet (called from MeshCore)
val packet = BitchatPacket(/* … */)
val messageId = UUID.randomUUID().toString()
storeForwardManager.cacheMessage(packet, messageId)

// 2️⃣ When a peer reconnects, forward any pending packets
storeForwardManager.sendCachedMessages(peerId)

// 3️⃣ Query how many messages are waiting for a specific peer
val pending = storeForwardManager.getCachedMessageCount(peerId)
Log.d("SF", "Pending messages for $peerId: $pending")

```

## Summary

- **[`StoreForwardManager.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/StoreForwardManager.kt)** implements the complete store-and-forward lifecycle: caching, deduplication, forwarding, and cleanup.
- The manager relies on `StoreForwardManagerDelegate` to query peer favorites, online status, and to send packets, remaining transport-agnostic.
- Messages are split between a regular `messageCache` and a per-peer `favoriteMessageQueue`, each with hard capacity limits.
- A periodic coroutine cleans up entries older than 12 hours and shrinks the `deliveredMessages` deduplication set every 10 minutes.
- On peer reconnect, cached packets are sorted by timestamp and forwarded with a 10 ms pacing delay to prevent flooding.

## Frequently Asked Questions

### How does Bitchat Android prevent duplicate messages when a peer reconnects?

The manager maintains a `deliveredMessages` set that records every message ID successfully forwarded. Before transmitting a cached packet, `sendCachedMessages` filters out any ID already present in this set, guaranteeing exactly-once delivery semantics for stored packets.

### What is the difference between favorite and regular peer message caching?

Favorite peers receive a dedicated `ConcurrentHashMap` queue with a high limit (`MAX_CACHED_MESSAGES_FAVORITES`), while regular peers share a single synchronized `messageCache` capped at `MAX_CACHED_MESSAGES`. This prioritization ensures that messages to important contacts survive longer and experience less eviction pressure.

### How long does Bitchat Android keep undelivered messages?

Regular cached messages are evicted after `MESSAGE_CACHE_TIMEOUT`, approximately 12 hours, by the periodic cleanup coroutine. Favorite queues are not subject to the same age-based eviction, though they remain bounded by their own capacity limit.

### Which Kotlin class manages store-and-forward for offline peers?

The entire mechanism is centralized in `StoreForwardManager`, defined in [`app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt). It is instantiated by [`MeshCore.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/MeshCore.kt) and driven by delegate callbacks supplied from [`BluetoothMeshService.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/BluetoothMeshService.kt) and [`WifiAwareMeshService.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/WifiAwareMeshService.kt).