BitChat Message Freshness Policies and TTL Capping: A Complete Guide

BitChat controls message propagation and storage lifetimes through hop-based TTL limits and age-based expiry windows defined in TransportConfig.swift, capping mesh packet travel at 7 hops in sparse networks and 5 in dense topologies while expiring stored public messages after 6 hours.

BitChat, developed by the permissionlesstech/bitchat repository, implements a strict freshness model to prevent mesh network flooding and bound storage growth. The BitChat message freshness policies combine time-to-live (TTL) counters for packet routing with age-based eviction for persisted data, all centralized in the TransportConfig type for auditability. These mechanisms ensure that messages either reach their destination within a limited hop budget or are discarded to maintain network efficiency.

Core Freshness Policies Defined in TransportConfig

All freshness limits reside in bitchat/Services/TransportConfig.swift, providing a single source of truth for mesh behavior and storage management.

Mesh Packet TTL Defaults

The default hop limit for regular mesh packets is 7 hops, defined as TransportConfig.messageTTLDefault at lines 8–9. When a packet originates, its TTL header is set to this value; each intermediate hop decrements the counter until it reaches zero and the packet is dropped.

Fragment Relay TTL Caps

Media fragmentation uses topology-aware TTL capping to prevent broadcast storms:

  • Sparse graphs: bleFragmentRelayTtlCap is set to 7 (line 29), matching the regular packet default to ensure fragments travel as far as text messages.
  • Dense graphs: bleFragmentRelayTtlCapDense is capped at 5 (line 30), reducing propagation depth to prevent overwhelming highly connected networks.

Storage and Sync Age Limits

Local storage enforces strict age limits to bound database size:

  • Public message sync: syncPublicMessageMaxAgeSeconds limits broadcasts to 6 hours (line 74).
  • General sync messages: syncMaxMessageAgeSeconds caps walk-synced messages at 15 minutes (line 69).
  • Private media receipts: privateMediaReceivedLedgerTTLSeconds retains receipts for 7 days (lines 21–24) before eviction.
  • Pre-key bundles: syncPrekeyBundleMaxAgeSeconds validates store-and-forward encryption keys for 24 hours (line 104).
  • Location drops: locationDropExpirySeconds expires geo-notes after 24 hours (line 26).
  • Deduplication cache: messageDedupMaxAgeSeconds suppresses duplicate IDs for 5 minutes (line 32).

How TTL Capping Works in RelayController

The RelayController.swift file enforces TTL constraints when forwarding packets. At line 26, the controller calculates the effective TTL for the next hop:

let ttlCap = min(ttl, TransportConfig.messageTTLDefault)

Sparse topology handling applies bleFragmentRelayTtlCap (7), allowing fragments to traverse the full mesh depth. Dense topology handling applies bleFragmentRelayTtlCapDense (5), cutting propagation short to prevent fragment storms while maintaining local delivery.

The hop count for any received packet is recoverable via originTTL – receivedTTL, implemented in localPackages/BitFoundation/Sources/BitFoundation/MeshPingPayload.swift.

Storage Freshness and Message Eviction

When the background sync task processes public timelines, it discards any entry older than syncPublicMessageMaxAgeSeconds (6 hours). This prevents stale broadcasts from persisting indefinitely. Private-media receipts follow a longer 7-day retention window via privateMediaReceivedLedgerTTLSeconds, after which they are purged to reclaim storage while maintaining enough history for reliable delivery confirmation.

Practical Implementation Examples

Creating a Mesh Packet with TTL

To originate a message with the correct freshness metadata:

import BitFoundation

let payload = try MeshPingPayload(
    nonce: Data([0x01, 0x02]), 
    originTTL: TransportConfig.messageTTLDefault
)
let packet = BitchatPacket(
    type: MessageType.ping.rawValue,
    ttl: TransportConfig.messageTTLDefault,
    payload: payload.toData()
)

Validating Message Age Before Display

To check if a stored message remains fresh according to the public sync policy:

func shouldDisplay(_ messageTimestamp: UInt64) -> Bool {
    let age = Date().timeIntervalSince1970 - Double(messageTimestamp) / 1000.0
    return age < TransportConfig.syncPublicMessageMaxAgeSeconds
}

Summary

  • Hop limits: Default mesh TTL is 7 hops; dense networks reduce fragment relay to 5 hops.
  • Storage caps: Public messages expire after 6 hours; private media receipts last 7 days.
  • Configuration: All policies are centralized in TransportConfig.swift for easy auditing.
  • Enforcement: RelayController.swift caps TTL at line 26; MeshPingPayload.swift tracks hop counts.

Frequently Asked Questions

What is the default TTL for BitChat mesh packets?

The default TTL is 7 hops, defined as TransportConfig.messageTTLDefault in bitchat/Services/TransportConfig.swift at lines 8–9. This value represents the maximum number of intermediary devices a packet can traverse before being automatically discarded by the mesh layer.

How does BitChat prevent fragment flooding in dense networks?

BitChat uses topology-aware capping via TransportConfig.bleFragmentRelayTtlCapDense, which sets a hard limit of 5 hops for fragment-relay packets in dense graphs (line 30 of TransportConfig.swift). The RelayController.swift enforces this cap at line 26 by taking the minimum of the current TTL and the configured cap, preventing media storms while preserving local delivery.

How long does BitChat retain private media receipts?

Private media receipts are retained for 7 days (604,800 seconds) as specified by TransportConfig.privateMediaReceivedLedgerTTLSeconds at lines 21–24 of TransportConfig.swift. After this period, the background eviction task purges these entries to prevent unbounded storage growth while maintaining sufficient history for delivery confirmation.

Where is the message freshness policy configured in BitChat source code?

All freshness policies are centralized in bitchat/Services/TransportConfig.swift, which defines constants for TTL defaults, fragment caps, sync age limits, and storage eviction windows. This single-file configuration allows operators to audit and tune the entire freshness model without modifying routing logic in RelayController.swift or storage logic elsewhere.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →