# How BitChat Handles Multi-Hop Relay: TTL and Packet Forwarding Explained

> Learn how BitChat manages multi-hop relay with TTL decrement and packet forwarding. Discover its deterministic relay policy for reliable delivery in any network.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-05

---

**BitChat uses a deterministic relay policy in `RelayController.decide` that combines per-hop TTL decrement with mesh topology awareness to control flooding and ensure reliable delivery across sparse and dense networks.**

The BitChat protocol implements a sophisticated **multi-hop relay system** that balances message reachability with network congestion control. This article examines how the protocol initializes, caps, and decrements TTL values, and how it applies different forwarding rules based on packet type and local node degree.

## TTL Initialization and Packet Header Storage

Every outgoing message in BitChat receives a **default TTL of 7** unless the sender explicitly overrides it. This value is defined in [`TransportConfig.swift`](https://github.com/permissionlesstech/bitchat/blob/main/TransportConfig.swift):

```swift
// TransportConfig.swift
static let messageTTLDefault = 7

```

The TTL is embedded in the packet header and travels with the message. Each relaying node decrements this counter by exactly one before forwarding. This simple mechanism provides an upper bound on how far any packet can propagate through the mesh.

## The Core Decision Engine: RelayController.decide

All forwarding logic converges in **`RelayController.decide`** ([`bitchat/Services/RelayController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/RelayController.swift)). This function evaluates twelve Boolean flags and the current node's degree to determine three outcomes: whether to relay, what TTL to assign, and how long to delay.

### Early Termination Rules

The controller applies aggressive filtering before expensive processing:

- **Link-local packets** (`REQUEST_SYNC` with TTL 0) are dropped immediately (lines 28-32)
- **TTL ≤ 1 after capping** triggers suppression (lines 34-37)
- **Self-originated or self-addressed** packets are never forwarded

```swift
// RelayController.swift - early suppression logic
if effectiveTTL <= 1 || senderIsSelf || recipientIsSelf {
    return RelayDecision(shouldRelay: false, newTTL: 0, delayMs: 0)
}

```

## Traffic-Specific Forwarding Policies

BitChat distinguishes four traffic classes, each with distinct TTL handling.

### Directed and Handshake Traffic

Session-critical packets—`handshake`, `directedEncrypted`, and `directedFragment`—receive **unconditional forwarding** with minimal latency:

- TTL decremented by exactly 1
- Jitter: 10-35 ms for handshakes, 20-60 ms for other directed traffic

This ensures reliable path establishment and encrypted session continuity across multiple hops.

### Fragment and Live-Voice Traffic

Media streams use **graph-aware TTL capping** to prevent flooding in dense networks:

| Mesh Density | Degree Threshold | TTL Cap |
|-------------|------------------|---------|
| Sparse | `< bleHighDegreeThreshold` (6) | `bleFragmentRelayTtlCap` = 7 |
| Dense | `≥ bleHighDegreeThreshold` (6) | `bleFragmentRelayTtlCapDense` = 5 |

The controller computes:

```swift
let cap = (degree >= highDegreeThreshold) 
    ? TransportConfig.bleFragmentRelayTtlCapDense 
    : TransportConfig.bleFragmentRelayTtlCap
let ttlLimit = min(ttl, cap)
let newTTL = ttlLimit - 1

```

This density-aware clamping prevents live voice fragments from overwhelming well-connected nodes while preserving reach in sparse chains.

### Broadcast Traffic

Public announces, board posts, and urgent messages follow a **degree-dependent TTL limit**:

- **High-degree nodes** (dense mesh): TTL limited to `min(ttlCap, 5)`, with floor of 2
- **Low-degree nodes** (sparse chains): retain full incoming TTL
- **Urgent board posts and announces**: preferred initial TTL = 7, otherwise = 6

```swift
// RelayController.swift lines 70-84
let ttlCap = isUrgentBoardPost || isAnnounce ? 7 : 6
let ttlLimit: Int
if degree >= highDegreeThreshold {
    ttlLimit = min(ttlCap, 5)
} else {
    ttlLimit = ttlCap
}
let newTTL = max(ttlLimit - 1, 0)

```

## Jitter Scheduling: Preventing Synchronized Floods

To avoid thundering herds, BitChat adds **degree-proportional random delays**:

| Node Degree | Delay Range |
|-------------|-------------|
| 0-2 | 10-40 ms |
| 3-5 | 60-150 ms |
| 6-9 | 80-180 ms |
| ≥ 10 | 100-220 ms |

Higher-degree nodes—those most at risk of contributing to broadcast storms—wait longer before relaying. This spreads traffic temporally and improves duplicate suppression effectiveness.

## Hop-Count Calculation for Diagnostics

The **Mesh Ping** diagnostic tool derives actual hop count from TTL values:

```swift
// MeshPingPayload.swift lines 50-56
var hopCount: Int {
    return originTTL - receivedTTL + 1
}

```

By comparing the originally transmitted TTL against the received value, the protocol computes precise path length without dedicated hop-count fields.

## Signature Handling and TTL Mutability

Because TTL changes during relay, it must be **excluded from cryptographic signatures**. In [`BitchatPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatPacket.swift) (lines 49-53), the signing process zeros the TTL field before hashing, then restores the actual value for transmission:

```swift
let packetForSigning = self.withTTL(0)  // TTL excluded from signature
let signature = try privateKey.sign(packetForSigning.serialized)
// Transmit with original TTL value

```

This allows in-flight TTL modification without invalidating authentication.

## Practical Examples

### Forwarding a Public Announce

```swift
let decision = RelayController.decide(
    ttl: 7,
    senderIsSelf: false,
    recipientIsSelf: false,
    isEncrypted: false,
    isDirectedEncrypted: false,
    isFragment: false,
    isDirectedFragment: false,
    isHandshake: false,
    isAnnounce: true,
    isRequestSync: false,
    isUrgentBoardPost: false,
    isVoiceFrame: false,
    degree: 4,
    highDegreeThreshold: TransportConfig.bleHighDegreeThreshold
)
// Low-degree node (4 < 6): relays with TTL 6, delay 60-150 ms

```

### Relaying Voice in Dense Mesh

```swift
let voiceDecision = RelayController.decide(
    ttl: 7,
    // ... other flags ...
    isFragment: true,
    isVoiceFrame: true,
    degree: 8,  // exceeds threshold
    highDegreeThreshold: 6
)
// Dense network: TTL capped at 5, then decremented to 4
// Delay: 80-180 ms

```

## Summary

- **TTL default is 7** in `TransportConfig.messageTTLDefault`, decremented once per hop
- **`RelayController.decide`** implements all forwarding logic with traffic-class differentiation
- **Link-local packets** (TTL 0) and expired TTL (≤ 1) are dropped immediately
- **Directed/handshake traffic** receives priority forwarding with minimal jitter
- **Media fragments** use density-aware TTL caps (7 sparse, 5 dense)
- **Broadcast traffic** applies degree-dependent limits to control flooding
- **Jitter scales with node degree** to prevent synchronized storms
- **Hop count** derives from `originTTL - receivedTTL + 1` in diagnostics
- **Signatures exclude TTL** to permit in-flight modification

## Frequently Asked Questions

### What happens when a packet's TTL reaches zero in BitChat?

Packets with TTL ≤ 1 after capping are suppressed and never forwarded. This hard limit prevents infinite propagation. The `RelayController` returns `shouldRelay: false` for these cases, effectively terminating the flood.

### How does BitChat prevent broadcast storms in dense networks?

Three mechanisms work together: **TTL caps** reduce propagation depth for high-degree nodes, **degree-proportional jitter** desynchronizes relays, and **traffic-specific policies** apply stricter limits to high-bandwidth media fragments than to small control messages.

### Can senders override the default TTL in BitChat?

Yes. While `TransportConfig.messageTTLDefault` provides a standard value of 7, the packet construction API accepts arbitrary TTL values. However, the `RelayController` clamps any incoming TTL to `min(providedTTL, messageTTLDefault)` before processing, preventing abuse.

### Why does BitChat zero the TTL field when signing packets?

Cryptographic signatures must remain valid across hops, but TTL necessarily changes during relay. By setting TTL to 0 for the signing operation and restoring the actual value afterward, the protocol achieves both authenticated origin verification and mutable hop limits.