# How BitChat Retries Unroutable Private Messages: The Claim-and-Release Mechanism Explained

> Learn how BitChat's claim-and-release mechanism retries unroutable private messages by removing failed IDs and reattempting transmission. Discover the core of BitChat's message reliability.

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

---

**BitChat implements a claim-and-release pattern that automatically retries unroutable read receipts by removing failed message IDs from a tracking set when `MessageRouter` returns `false`, enabling the next periodic read-scan to attempt retransmission.**

BitChat's private messaging protocol ensures reliable delivery of read receipts even when destination peers are temporarily unreachable. The retry mechanism for unroutable private messages operates through a lightweight state-tracking system in [`PrivateChatManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PrivateChatManager.swift) that prevents duplicate transmissions while guaranteeing eventual delivery.

## The Core Challenge: Unreachable Peers in P2P Networks

In decentralized messaging systems, peers frequently go offline or become temporarily unroutable due to network partitions. When BitChat attempts to send a read receipt through the `MessageRouter`, the transport layer may fail to establish a path to the recipient. Rather than permanently dropping these failed receipts, BitChat employs an elegant state machine that defers transmission until connectivity restores.

## How the Claim-and-Release Retry Pattern Works

The retry mechanism centers on three coordinated operations that ensure exactly-once delivery semantics without blocking the UI thread.

### Claiming the Receipt ID

Before attempting transmission, the `PrivateChatManager` claims the receipt by inserting the message ID into a `Set<String>` called `sentReadReceipts`. This insertion acts as a distributed lock, preventing duplicate send attempts while an initial routing operation is in progress.

```swift
sentReadReceipts.insert(messageID)          // claim the receipt

```

### Detecting Routing Failures

The `MessageRouter` provides a Boolean return value from `sendReadReceipt(_:to:)` indicating routing success. When this method returns `false`, signaling that the peer is currently unreachable, the manager immediately releases the claim.

```swift
if !router.sendReadReceipt(receipt, to: senderPeerID) {
    // Unroutable → remove the claim so the next read‑scan will retry
    self?.sentReadReceipts.remove(messageID)
}

```

This removal makes the receipt eligible for immediate reprocessing without requiring complex polling logic or persistent queue management.

### Automatic Retry via Periodic Read-Scans

BitChat triggers "read-scan" cycles whenever the chat view appears or new messages arrive. During these scans, the manager iterates through pending receipts and attempts to send any ID not present in `sentReadReceipts`. Because unroutable receipts are removed from the set upon failure, they naturally bubble back up for retry during the next scan cycle.

## Implementation in PrivateChatManager.swift

The core logic resides in [`bitchat/Services/PrivateChatManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/PrivateChatManager.swift) between lines 64-78. The implementation uses Swift concurrency to ensure thread safety while maintaining responsive UI performance.

```swift
if let router = messageRouter {
    SecureLogger.debug("… sending READ ack … via router", category: .session)
    sentReadReceipts.insert(messageID)          // claim the receipt
    Task { @MainActor [weak self] in
        // `router.sendReadReceipt` returns *false* if the receipt cannot be routed
        // (e.g. the peer is currently unreachable)
        if !router.sendReadReceipt(receipt, to: senderPeerID) {
            // Unroutable → remove the claim so the next read‑scan will retry
            self?.sentReadReceipts.remove(messageID)
        }
    }
}

```

The `@MainActor` annotation ensures UI-related logging and state updates occur on the main thread, while the weak self capture prevents retain cycles in the asynchronous Task closure.

## Practical Retry Flow Example

When marking messages as read through the public API, the retry mechanism operates transparently:

```swift
// Mark messages as read → automatically sends read receipts
await chatManager.markAsRead(from: peerID)

// Internally:
// 1. `sendReadReceipt` claims the receipt ID
// 2. Tries to route via `MessageRouter`
// 3. On failure, the claim is removed → next scan retries

```

For debugging or testing purposes, you can simulate the retry logic manually:

```swift
// Simulated retry flow
let receiptID = "msg-12345"
chatManager.sentReadReceipts.insert(receiptID)   // claim
let routed = router.sendReadReceipt(receipt, to: peerID)

if !routed {
    // Unroutable → make the receipt eligible for retry
    chatManager.sentReadReceipts.remove(receiptID)
    // Next read-scan will pick this up automatically
}

```

## Integration with the MessageRouter Interface

The retry mechanism depends on a strict contract with [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift). The `sendReadReceipt(_:to:)` method must return a Boolean indicating routing feasibility rather than throwing exceptions or blocking indefinitely. This design allows the `PrivateChatManager` to make immediate decisions about state management without managing complex error hierarchies or timeout logic.

## Summary

- **Claim-and-release pattern**: BitChat inserts message IDs into `sentReadReceipts` before routing and removes them only upon confirmed failure, creating a self-healing retry loop.
- **Failure detection**: The `MessageRouter.sendReadReceipt` method returns `false` for unroutable peers, triggering immediate claim removal in [`PrivateChatManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PrivateChatManager.swift).
- **Automatic retry**: Periodic read-scans (triggered by UI activity or new messages) automatically pick up unclaimed receipts for retransmission without manual intervention.
- **Thread safety**: The implementation uses Swift's `@MainActor` concurrency model to safely mutate shared state from asynchronous network callbacks.

## Frequently Asked Questions

### How does BitChat prevent duplicate read receipts during retries?

BitChat prevents duplicates through the `sentReadReceipts` set, which acts as a claim mechanism. Once an ID enters this set, subsequent read-scans skip that receipt until the routing attempt completes. Only upon explicit failure (return value `false` from `MessageRouter`) does the system remove the ID, making it eligible for exactly one retry attempt.

### What triggers a retry for unroutable private messages?

Retries trigger automatically during periodic "read-scans" that execute when the chat view appears or new messages arrive. The system scans for receipts not present in `sentReadReceipts`; since unroutable receipts are removed from this set immediately upon failure, they become visible to the next scan cycle without requiring explicit retry timers or exponential backoff logic.

### Where is the retry logic implemented in the BitChat codebase?

The retry mechanism is implemented in [`bitchat/Services/PrivateChatManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/PrivateChatManager.swift), specifically between lines 64-78. This file manages the `sentReadReceipts` state and coordinates with [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift) to determine routing feasibility. The boolean return contract from `sendReadReceipt(_:to:)` drives the claim-release decision tree.

### What happens if a peer remains permanently unreachable?

If a peer remains permanently unreachable, the receipt ID will continuously cycle through the claim-release pattern. The ID remains outside `sentReadReceipts` (due to routing failures), causing each read-scan to attempt transmission again. While this creates redundant network attempts, the lightweight nature of read receipts and the set-based deduplication ensure the system does not accumulate unbounded memory or duplicate deliveries once connectivity eventually restores.