# How BitChat Manages Message TTL Expiration in the Outbox: Implementation Guide

> Discover how BitChat manages message TTL expiration in the outbox using a three-layered defense mechanism in MessageRouter.swift. Learn about periodic sweeps, validation, and retry checks.

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

---

**BitChat enforces a strict 24-hour Time-to-Live (TTL) policy for queued private messages through a three-layered defense mechanism in [`MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessageRouter.swift), combining periodic background sweeps, flush-time validation, and secure-retry checks to automatically purge stale messages from memory and encrypted disk storage.**

BitChat, an open-source peer-to-peer messaging application maintained by `permissionlesstech/bitchat`, implements a store-and-forward outbox system that guarantees messages do not linger indefinitely. Understanding how BitChat manages message TTL expiration in the outbox is critical for developers building on the protocol or debugging message delivery failures. The system guarantees that any undelivered private message queued in the outbox is automatically removed after exactly 24 hours through multiple redundant enforcement points.

## TTL Architecture Overview

The TTL enforcement strategy relies on a centralized constant and three distinct execution phases that ensure no expired message survives regardless of the code path taken.

### The 24-Hour Constant Definition

In [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift), the TTL duration is defined as a static constant:

```swift
private static let messageTTLSeconds: TimeInterval = 24 * 60 * 60 // 24 hours

```

This value serves as the single source of truth for all expiration calculations across the routing layer. According to the BitChat source code, every queued message carries a `timestamp` property recorded at enqueue time, and the router calculates age using `now.timeIntervalSince(message.timestamp)` compared against this constant.

### Three-Layer Enforcement Points

BitChat implements defense-in-depth through three specific checkpoints:

1. **Periodic Background Sweep** – The `startBridgeDepositSweep` task runs every two minutes and invokes `cleanupExpiredMessages()` to scan the entire outbox.
2. **Flush-Time Validation** – Whenever `flushOutbox(for:)` prepares to transmit a peer’s queued messages, it filters entries older than `messageTTLSeconds` before attempting delivery.
3. **Secure-Retry Checks** – During session recovery, `retrySecurePrivateMessagesAfterAuthentication` validates TTL before reattempting delivery of pending secure messages.

## Message Expiration Mechanics

The expiration logic follows a consistent pattern across all enforcement points, combining in-memory cleanup with encrypted persistence updates.

### Age Calculation and Dropping Logic

When evaluating a `QueuedMessage`, the router performs the following calculation:

```swift
let age = now.timeIntervalSince(message.timestamp)
if age > Self.messageTTLSeconds {
    // Trigger expiration path
}

```

Upon expiration detection, the system:
- Removes the entry from the in-memory `outbox` dictionary via `removeQueuedMessage`
- Invokes `dropMessage` to log the event, increment the `outboxDropped` metric, and trigger UI callbacks through `onMessageDropped`
- Persists the modified outbox state to disk

### Persistence Update Flow

The `MessageOutboxStore` handles encrypted on-disk storage but does **not** enforce TTL itself. After any removal operation, the router explicitly calls `persistOutbox()`, which writes the current snapshot to the encrypted store. This architecture separates policy enforcement (router) from data persistence (store), ensuring the store remains agnostic to business logic.

## Code Implementation Details

The following sections detail the specific implementation locations and patterns used to enforce the 24-hour limit.

### Periodic Cleanup Implementation

The background maintenance task initiates cleanup every two minutes:

```swift
// Inside startBridgeDepositSweep (lines 6-9)
func startBridgeDepositSweep() {
    // Other bridge logic...
    cleanupExpiredMessages()
}

```

The `cleanupExpiredMessages()` method iterates through every peer’s message queue, applying the TTL calculation and removing expired entries. This prevents stale messages from accumulating during periods of low connectivity.

### Flush-Time Validation

Before transmitting queued messages to a peer, `flushOutbox(for:)` performs a final TTL check (lines 107-115):

```swift
func flushOutbox(for peerID: PeerID) {
    guard let messages = outbox[peerID] else { return }
    
    let validMessages = messages.filter { message in
        let age = Date().timeIntervalSince(message.timestamp)
        guard age <= Self.messageTTLSeconds else {
            dropMessage(message, reason: .expired)
            return false
        }
        return true
    }
    
    // Proceed with sending validMessages...
}

```

This guards against race conditions where a message might expire between the periodic sweep and the actual transmission attempt.

### Secure Retry Handling

For encrypted sessions requiring authentication, `retrySecurePrivateMessagesAfterAuthentication` (lines 55-57) applies the same TTL guard before retrying delivery. This ensures that messages do not spuriously retry after the recipient comes online if the 24-hour window has already elapsed.

## Practical Usage and Monitoring

Developers interacting with the BitChat outbox can observe and manually trigger TTL behavior through the following patterns.

**Enqueueing a message automatically sets the timestamp:**

```swift
router.sendPrivate(
    "Hey, see you at 5 pm",
    to: peerID,
    recipientNickname: "Alice",
    messageID: UUID().uuidString
)

```

**Forcing immediate cleanup (useful in testing):**

```swift
router.cleanupExpiredMessages()

```

**Monitoring expiration events via callback:**

```swift
router.onMessageDropped = { messageID, peerID in
    print("Message \(messageID) for \(peerID.id) expired and was removed")
}

```

Each expiration generates debug logging (e.g., "⏰ Expired queued message …") and updates the `outboxDropped` metric, enabling operational monitoring of TTL effectiveness.

## Summary

- **BitChat enforces a strict 24-hour TTL** defined by `messageTTLSeconds` in [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift).
- **Three redundant checkpoints**—periodic sweeps, flush-time validation, and secure-retry checks—ensure comprehensive expiration coverage.
- **Expired messages trigger** `dropMessage()`, which removes entries from memory, updates metrics, notifies UI callbacks, and triggers persistence updates.
- **The MessageOutboxStore** handles encrypted disk storage but relies on the router to enforce TTL policy, calling `persistOutbox()` after modifications.
- **Background maintenance** runs every two minutes via `startBridgeDepositSweep`, preventing stale message accumulation.

## Frequently Asked Questions

### What is the default TTL for messages in BitChat?

BitChat defines a default TTL of **24 hours** (86,400 seconds) through the `messageTTLSeconds` constant in [`bitchat/Services/MessageRouter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/MessageRouter.swift). Any private message remaining in the outbox beyond this duration is automatically purged regardless of delivery status.

### How often does BitChat check for expired messages?

The system performs TTL validation at three intervals: a background sweep every **two minutes** via `startBridgeDepositSweep`, immediate checks during every outbox flush operation through `flushOutbox(for:)`, and validation during secure session recovery in `retrySecurePrivateMessagesAfterAuthentication`.

### Does the encrypted MessageOutboxStore enforce TTL automatically?

No. According to the BitChat source code, [`bitchat/Services/Courier/MessageOutboxStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Courier/MessageOutboxStore.swift) handles encrypted persistence but remains policy-agnostic. The `MessageRouter` enforces TTL logic in memory and explicitly calls `persistOutbox()` to write the filtered state to disk after removing expired entries.

### Can developers manually trigger TTL cleanup in BitChat?

Yes. Developers can invoke `router.cleanupExpiredMessages()` directly on the `MessageRouter` instance to force an immediate scan and removal of all expired messages. This is particularly useful for unit testing expiration logic or manually clearing stale queues during development.