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.swiftdefine 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.swiftmirrors 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →