How BitChat Manages Message TTL Expiration in the Outbox: Implementation Guide
BitChat enforces a strict 24-hour Time-to-Live (TTL) policy for queued private messages through a three-layered defense mechanism in 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, the TTL duration is defined as a static constant:
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:
- Periodic Background Sweep – The
startBridgeDepositSweeptask runs every two minutes and invokescleanupExpiredMessages()to scan the entire outbox. - Flush-Time Validation – Whenever
flushOutbox(for:)prepares to transmit a peer’s queued messages, it filters entries older thanmessageTTLSecondsbefore attempting delivery. - Secure-Retry Checks – During session recovery,
retrySecurePrivateMessagesAfterAuthenticationvalidates 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:
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
outboxdictionary viaremoveQueuedMessage - Invokes
dropMessageto log the event, increment theoutboxDroppedmetric, and trigger UI callbacks throughonMessageDropped - 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:
// 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):
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:
router.sendPrivate(
"Hey, see you at 5 pm",
to: peerID,
recipientNickname: "Alice",
messageID: UUID().uuidString
)
Forcing immediate cleanup (useful in testing):
router.cleanupExpiredMessages()
Monitoring expiration events via callback:
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
messageTTLSecondsinbitchat/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. 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →