BitChat Message Freshness Windows: How Time Limits Vary by Message Type

BitChat defines distinct freshness windows ranging from 15 minutes to 24 hours depending on message type, with ephemeral voice traffic and diagnostic pings having no persistence at all.

The permissionless mesh messenger BitChat uses freshness windows—hard time limits that determine whether a message remains eligible for gossip synchronization, acceptance, or re-broadcast. These windows are defined in TransportConfig.swift and enforced across the protocol stack. Understanding how they vary by message type helps explain BitChat's trade-offs between reliability, battery life, and security.

Core Freshness Constants Defined in TransportConfig.swift

All time-based limits originate from three constants in bitchat/Services/TransportConfig.swift:

// Public broadcast messages
syncPublicMessageMaxAgeSeconds = 6 × 60 × 60  // 6 hours

// Fragments and file transfers
syncMaxMessageAgeSeconds = 900                // 15 minutes

// Pre-key bundles for cryptographic handshakes
syncPrekeyBundleMaxAgeSeconds = 24 × 60 × 60  // 24 hours

These values propagate through the receive-side policy logic in BLEPublicMessagePolicy.swift and gateway enforcement in GatewayService.swift.

Message Type Freshness Window Reference

Message Type Freshness Window Source Constant Rationale
Public chat messages (message) 6 hours syncPublicMessageMaxAgeSeconds Long enough for mesh partition healing
Fragments & file transfers (fragment, fileTransfer) 15 minutes syncMaxMessageAgeSeconds Data valid only while sender is present
Pre-key bundles (prekeyBundle) 24 hours syncPrekeyBundleMaxAgeSeconds One-time keys must persist across device absence
Announce / AnnounceV2 (announce, announceV2) 6 hours Inherited from public messages Presence signals match chat message lifetime
Leave (leave) 6 hours Inherited from public messages Departure notices expire with presence state
Noise-encrypted traffic (noiseEncrypted) Payload-defined None Timestamp and signature inside encryption
Live voice bursts (voiceFrame) None (ephemeral) Comment in BLEService.swift Never stored, never synced
Gateway/Nostr carrier (nostrCarrier) 6 hours Comment in GatewayService.swift Matches receiver expectations
Ping/Pong (ping, pong) Seconds (implicit) Round-trip timing Diagnostic only, never persisted

Message Type Definitions in MessageType.swift

The protocol enumerates all message kinds in [MessageType.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/MessageType.swift):

public enum MessageType: UInt8, Sendable {
    case ping = 0
    case pong = 1
    case announce = 2
    case message = 3
    case prekeyBundle = 4
    case noiseEncrypted = 5
    case announceV2 = 6
    case fragment = 7
    case fileTransfer = 8
    case leave = 9
    case voiceFrame = 10
    case nostrCarrier = 11
}

Each type maps to freshness logic through the constants above and specialized handling in service layers.

How Freshness Enforcement Works

Receive-Side Filtering: BLEPublicMessagePolicy.swift

Incoming messages are validated against their freshness window in bitchat/Services/BLE/BLEPublicMessagePolicy.swift. Packets exceeding the age limit are dropped as stale before processing.

// Pseudocode based on source implementation
func accept(_ message: GossipMessage) -> Bool {
    let age = currentTime - message.timestamp
    switch message.type {
    case .message, .announce, .announceV2, .leave:
        return age <= syncPublicMessageMaxAgeSeconds
    case .fragment, .fileTransfer:
        return age <= syncMaxMessageAgeSeconds
    case .prekeyBundle:
        return age <= syncPrekeyBundleMaxAgeSeconds
    case .voiceFrame:
        return true // Always accept, never store
    default:
        return false
    }
}

Gateway Enforcement: GatewayService.swift

The Nostr bridge in [GatewayService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Gateway/GatewayService.swift) applies the same 6-hour window to prevent stale events from entering the mesh:

// Gateway validates nostrCarrier messages
// against syncPublicMessageMaxAgeSeconds
// before forwarding to BLE layer

Design Rationale: Why Windows Vary by Message Type

Short windows for transient data. Fragments and file transfers use 15 minutes because they represent incomplete data valid only while the sender maintains connectivity. Re-gossiping stale fragments wastes bandwidth on unrecoverable transfers.

Extended window for cryptographic infrastructure. Pre-key bundles receive 24 hours because they enable offline secure handshakes. A device joining the mesh hours after its peer published keys must still find them.

Ephemeral exclusion for real-time media. Voice frames bypass the gossip store entirely—they are broadcast once and discarded, matching users' expectation of ephemeral conversation.

Uniform treatment of presence signals. Announcements and leave notices share the 6-hour public message window because they describe the same temporal state: whether a peer is considered present in the mesh.

Summary

  • Three core constants in TransportConfig.swift define all freshness windows: 6 hours for public traffic, 15 minutes for fragments/file transfers, 24 hours for pre-key bundles
  • MessageType.swift enumerates 12 protocol message types, each mapped to appropriate freshness logic
  • BLEPublicMessagePolicy.swift enforces windows on receipt; GatewayService.swift mirrors enforcement for bridged traffic
  • Voice traffic and pings operate outside freshness windows—ephemeral by design or diagnostic by function
  • These limits bound gossip store growth, conserve battery and bandwidth, and balance security with partition tolerance

Frequently Asked Questions

What happens to messages that exceed their freshness window?

Messages older than their defined window are rejected by receiving peers and excluded from re-gossip. For public messages, this occurs in BLEPublicMessagePolicy.swift; the packet is dropped before processing and never stored in the local gossip cache.

Why do pre-key bundles have a longer freshness window than chat messages?

Pre-key bundles must survive 24 hours because they enable cryptographic handshakes with offline peers. A device returning to the mesh after hours away needs to discover its contacts' public keys to establish encrypted sessions. The 6-hour chat window would be too short for this asynchronous key distribution.

Can freshness windows be configured or overridden?

No user-facing configuration exists in the current implementation. All windows are compile-time constants in TransportConfig.swift. Changing them requires rebuilding the client and would affect mesh compatibility since all peers must agree on staleness criteria for gossip synchronization to function.

How does BitChat handle time synchronization between devices?

Freshness enforcement relies on local device time. The protocol does not implement explicit time synchronization; instead, it assumes reasonable clock accuracy (within minutes, not hours). Severe clock skew can cause false staleness rejections or acceptance of future-dated messages, though the cryptographic signatures in noiseEncrypted payloads provide additional temporal binding.

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 →