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

> Discover BitChat's message freshness windows, from 15 minutes to 24 hours based on message type. Learn how ephemeral messages have no persistence.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-04

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/TransportConfig.swift):

```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`](https://github.com/permissionlesstech/bitchat/blob/main/BLEPublicMessagePolicy.swift) and gateway enforcement in [`GatewayService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/BLEService.swift) | Never stored, never synced |
| **Gateway/Nostr carrier** (`nostrCarrier`) | **6 hours** | Comment in [`GatewayService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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/MessageType.swift)](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/MessageType.swift):

```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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEPublicMessagePolicy.swift). Packets exceeding the age limit are dropped as stale before processing.

```swift
// 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/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:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.