# How BitChat's Outbox Handles Message Retries: Swift Implementation Guide

> Learn how BitChat's outbox ensures Nostr message delivery with persistent in-memory storage and exponential backoff for reliable retries over unstable connections.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-08-08

---

**BitChat implements a persistent in‑memory outbox using `PendingSend` structs and exponential backoff to guarantee Nostr message delivery across unstable relay connections.**

BitChat, the decentralized messaging app from the [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat) repository, guarantees reliable message delivery through a sophisticated outbox mechanism. When network conditions fluctuate or relays temporarily disconnect, the app stores outbound events in a retry queue rather than dropping them. This article examines the Swift implementation in [`NostrRelayManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrRelayManager.swift) to explain exactly how the outbox manages message persistence, reconnection logic, and overflow protection.

## Core Outbox Data Structures

The retry system centers on two primary structures defined in [`bitchat/Nostr/NostrRelayManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrRelayManager.swift):

- **`messageQueue: [PendingSend]`** – An array that stores every outbound event awaiting acknowledgment. Each `PendingSend` instance encapsulates the `NostrEvent` and a `Set` of relays that have not yet accepted the message.
- **`ConfirmedSendState`** – A tracking mechanism for messages requiring explicit relay acknowledgments (such as gift‑wrapped private envelopes). This state monitors which relays must return an `OK` response before the message clears the outbox.

When `send(event:)` is invoked, the manager creates a `PendingSend` entry and appends it to `messageQueue` (lines 63‑70). The message remains queued until every target relay either acknowledges receipt or the queue exceeds its memory limits.

## The Message Retry Workflow

BitChat’s outbox operates on a "store‑and‑forward" principle with four distinct phases:

1. **Queue insertion** – The event wraps into a `PendingSend` along with its target relay set, then pushes onto `messageQueue`.

2. **Flush on reconnection** – The `flushMessageQueue()` method iterates over cached entries whenever a relay connection becomes viable. For each `PendingSend`, it attempts `connection.send(message)` on every relay still listed in `pendingRelays`.

3. **Error handling** – If a WebSocket send fails, the relay remains in the `pendingRelays` set. The entry stays in `messageQueue` for automatic retry during the next connection window.

4. **Acknowledgment verification** – For gift‑wrapped messages, `ConfirmedSendState` tracks pending `OK` responses. The outbox removes the entry only after every targeted relay replies; otherwise, retries continue indefinitely.

## Exponential Backoff and Jitter

To prevent thundering herds against struggling relays, BitChat applies calculated delays before reconnection attempts. The backoff configuration (lines 87‑90) uses three parameters:

- **`initialBackoffInterval`** – Base delay for the first retry attempt.
- **`maxBackoffInterval`** – Upper bound for the delay ceiling.
- **`backoffMultiplier`** – Factor by which the interval increases after each failed attempt.

The manager adds **`jitterUnit`** to these calculations, introducing randomization that desynchronizes retry storms across multiple relay connections. When `scheduleAfter(_:)` triggers a reconnection, the exponential backoff ensures the outbox retries messages with a geometrically increasing delay, respecting both network stability and relay load.

## Queue Management and Overflow Protection

Unbounded memory growth is mitigated through `pendingSendDropCount`. When `messageQueue` exceeds its configured capacity, the manager discards the oldest `PendingSend` entries and increments the drop counter. This circuit breaker triggers a UI warning, alerting users that some messages may require manual resend while preventing application memory exhaustion.

## Code Implementation Example

Below is a practical demonstration of how BitChat’s outbox handles a private message and recovers from transient failures:

```swift
import Foundation

// 1. Sending a gift-wrapped private message
let encryptedPayload = sealMessage(content: "Hello", to: recipientPubkey)
let event = NostrEvent(kind: .privateMessage, content: encryptedPayload)

NostrRelayManager.shared.send(event: event) { success in
    // This callback fires only after every default relay 
    // responds with an OK acknowledgment
    if success {
        print("Message confirmed by all relays")
    } else {
        print("Message remains in outbox for retry")
    }
}

// 2. Internal queue management (simplified view)
// Located in bitchat/Nostr/NostrRelayManager.swift
class NostrRelayManager {
    private var messageQueue: [PendingSend] = []
    private var confirmedSends: [String: ConfirmedSendState] = [:]
    
    func send(event: NostrEvent) {
        let pending = PendingSend(
            event: event,
            pendingRelays: Set(defaultRelays)
        )
        messageQueue.append(pending)
        flushMessageQueue()
    }
    
    func flushMessageQueue() {
        for (index, pending) in messageQueue.enumerated().reversed() {
            for relay in pending.pendingRelays {
                if let connection = activeConnections[relay] {
                    do {
                        try connection.send(pending.event)
                        // Remove relay from pending set on successful socket write
                        messageQueue[index].pendingRelays.remove(relay)
                    } catch {
                        // Relay stays in pendingRelays; retry on next flush
                    }
                }
            }
            
            // Cleanup if all relays processed
            if messageQueue[index].pendingRelays.isEmpty {
                messageQueue.remove(at: index)
            }
        }
    }
}

```

## Summary

- **Persistent Queue** – The `messageQueue` array stores `PendingSend` objects that bind each event to its remaining target relays.
- **Automatic Retry** – Failed sends retain the relay in `pendingRelays`, triggering automatic resend during the next `flushMessageQueue()` cycle.
- **Backoff Strategy** – Exponential backoff with randomized jitter (configured in lines 87‑90) spaces reconnection attempts to avoid overwhelming relays.
- **Memory Safety** – `pendingSendDropCount` enforces a ceiling on queue length, dropping oldest entries and warning users when resources are constrained.
- **Acknowledgment Tracking** – `ConfirmedSendState` ensures gift‑wrapped messages receive explicit relay confirmation before clearing the outbox.

## Frequently Asked Questions

### How does BitChat prevent message loss during network outages?

BitChat persists unsent messages in the `messageQueue` array within `NostrRelayManager`. Rather than discarding failed transmissions, the outbox retains the event alongside its `pendingRelays` set. When connectivity resumes, `flushMessageQueue()` automatically retries delivery to every relay that has not yet acknowledged the message.

### What triggers a message retry in the BitChat outbox?

Retries trigger in two scenarios: when `flushMessageQueue()` runs after a successful relay reconnection (scheduled via exponential backoff), or when the user sends a new event that prompts the manager to re‑evaluate pending items. The `scheduleAfter(_:)` method manages these reconnection windows using jittered delays defined in lines 87‑90 of [`NostrRelayManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrRelayManager.swift).

### Does BitChat implement exponential backoff for relay connections?

Yes. The `NostrRelayManager` configures backoff parameters including `initialBackoffInterval`, `maxBackoffInterval`, and `backoffMultiplier`. Failed connections trigger `scheduleAfter(_:)` with geometrically increasing delays, plus a `jitterUnit` randomization factor to prevent synchronized retry storms across multiple relay clients.

### What happens when the BitChat message queue grows too large?

When `messageQueue` exceeds its memory threshold, the manager increments `pendingSendDropCount` and removes the oldest `PendingSend` entries. This protects against unbounded memory growth while surfacing a UI warning that notifies users certain messages may need manual resending.