# How the Relay Policy Dictates Message Forwarding in the Bitchat Mesh

> Discover how Bitchat's relay policy uses RelayController.decide to determine message forwarding in the mesh based on packet type, TTL, and topology. Learn about hop delays and new TTL values.

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

---

**Bitchat’s deterministic relay policy uses the `RelayController.decide` method to evaluate packet types, TTL constraints, and mesh topology, returning a structured decision that dictates whether to forward, the new TTL, and the precise delay milliseconds for each hop.**

Bitchat is an open-source, permissionless messaging mesh built by permissionlesstech. Understanding how its **relay policy dictates message forwarding within the mesh** is essential for optimizing message propagation and preventing network congestion in decentralized environments.

## Core Decision Logic in RelayController

The central authority for flood control is the `RelayController` type defined in [`bitchat/Services/RelayController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/RelayController.swift). Its static method `RelayController.decide` implements a deterministic algorithm that evaluates high-level packet flags against current mesh topology to produce a `RelayDecision`.

### The Five-Step Evaluation Algorithm

The method processes incoming packets through a strict priority hierarchy:

| Step | Condition | Action |
|------|-----------|--------|
| 1 | `isRequestSync` (link-local) | Never relay – return `shouldRelay = false` |
| 2 | `ttlCap ≤ 1` **or** `senderIsSelf` **or** `recipientIsSelf` | Suppress obvious non-relays |
| 3 | **Session-critical** traffic (`isHandshake`, `isDirectedFragment`, `isDirectedEncrypted`) | Always relay with single-hop TTL decrement and modest jitter (10-35 ms for handshakes, 20-60 ms otherwise) |
| 4 | **Live media** (`isFragment` or `isVoiceFrame`) | Apply fragment policy: determine TTL cap based on node degree (`bleFragmentRelayTtlCapDense` vs `bleFragmentRelayTtlCap`); relay if capped TTL > 1 with random delay within BLE fragment window |
| 5 | **Broadcast / announce** traffic | Choose TTL limit by topology: dense graphs ≤ 5, thin chains allow full TTL, urgent posts get boost to 7; delay chosen from tiered range growing with degree |

The output is a `RelayDecision` struct containing `shouldRelay` (Boolean), `newTTL` (UInt8), and `delayMs` (UInt64). This structure limits flood-overload while guaranteeing timely delivery for critical traffic.

## Integration with Source Routing

When a node receives a packet **not addressed to itself**, the mesh first checks for a source-route (v2) flag. According to [`docs/SOURCE_ROUTING.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md), if a route exists:

1. The node finds its own peer ID in the route list.
2. It forwards the packet to the **next hop** (`i + 1`).
3. If the next hop is unreachable, it falls back to broadcast/flood.

If no route is present, the packet follows the classic flood path subject to the TTL and probability limits defined by `RelayController`. The `BLESourceRouteOriginationPolicy` determines whether to attach a route at send-time, requiring the packet to be locally authored, directed, have TTL headroom, and guarantee a viable v2 path.

## Where the Policy Is Applied

The relay policy operates at critical junctions in the message pipeline:

- **BLE receive pipeline** ([`bitchat/Services/BLE/BLEReceivePipeline.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEReceivePipeline.swift)): Invokes `RelayController.decide` for each incoming fragment.
- **MessageRouter**: Uses the decision to schedule delayed sends or drop packets.
- **Transport configuration** ([`bitchat/Services/TransportConfig.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/TransportConfig.swift)): Stores TTL caps and jitter ranges referenced by the controller.

### Code Examples

The following Swift code demonstrates how to evaluate an incoming fragment:

```swift
// Example: deciding whether to forward an incoming fragment
let decision = RelayController.decide(
    ttl: incomingPacket.ttl,
    senderIsSelf: incomingPacket.sender == myPeerID,
    recipientIsSelf: false,
    isEncrypted: true,
    isDirectedEncrypted: false,
    isFragment: true,
    isDirectedFragment: false,
    isHandshake: false,
    isAnnounce: false,
    isRequestSync: false,
    degree: meshDegree,
    highDegreeThreshold: 5)

// Schedule the forward if permitted
if decision.shouldRelay {
    scheduler.schedule(after: .milliseconds(decision.delayMs)) {
        sendPacket(packet, ttl: decision.newTTL)
    }
}

```

When originating packets, the source-route policy works alongside the relay controller:

```swift
// Example: attaching a source-route only when the origination policy permits
if BLESourceRouteOriginationPolicy.shouldAttachRoute(
    packet: outgoingPacket,
    myPeerID: myPeerID,
    mesh: meshTopology) {
    outgoingPacket.attachRoute(...)
}
else {
    // Fall back to normal flood; RelayController will handle TTL/delay
}

```

## Summary

- **`RelayController.decide`** in [`bitchat/Services/RelayController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/RelayController.swift) serves as the central authority for all forwarding decisions.
- The policy uses a **five-step hierarchy** that prioritizes session-critical traffic over bulk media and suppresses link-local sync requests.
- **TTL caps and jitter windows** are dynamically adjusted based on node degree to prevent exponential fan-out in dense mesh topologies.
- **Source routing** provides deterministic paths when available, falling back to the relay-controlled flood mechanism when routes are absent or invalid.
- Configuration constants for delays and caps reside in [`TransportConfig.swift`](https://github.com/permissionlesstech/bitchat/blob/main/TransportConfig.swift), allowing protocol tuning without modifying core logic.

## Frequently Asked Questions

### How does the relay policy handle high-traffic voice frames?

Live media such as voice frames (`isVoiceFrame`) and fragments (`isFragment`) trigger the **fragment policy** within `RelayController.decide`. The system checks node degree against `highDegreeThreshold` to select between `bleFragmentRelayTtlCapDense` and `bleFragmentRelayTtlCap`. High-degree nodes receive stricter TTL limits to prevent broadcast storms, while all fragment relays include random delays within `bleFragmentRelayMinDelayMs…MaxDelayMs` to desynchronize transmissions.

### What happens when a packet has a source route attached?

When a packet carries a source-route list, the receiving node bypasses the standard flood logic and forwards directly to the next hop in the route list, as documented in [`docs/SOURCE_ROUTING.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/SOURCE_ROUTING.md). If that next hop is unreachable, the node falls back to the standard relay policy for broadcast distribution. This hybrid approach optimizes for efficiency when paths are known while maintaining reliability through flood fallback.

### Why does the policy suppress packets with `ttlCap ≤ 1`?

Packets with a TTL (Time To Live) of 1 or less have no remaining propagation budget. The `RelayController` suppresses these immediately to prevent unnecessary airtime consumption and processing overhead. This check occurs early in the decision pipeline (Step 2) alongside self-origination and self-destination checks to quickly eliminate non-viable candidates before evaluating more complex policy rules.

### Where are the delay and TTL configuration values defined?

The numerical parameters for the relay policy—including `bleFragmentRelayTtlCap`, `bleFragmentRelayMinDelayMs`, and tiered broadcast delay ranges—are defined in [`bitchat/Services/TransportConfig.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/TransportConfig.swift). This separation of configuration from logic allows developers to tune mesh behavior for different deployment scenarios (dense urban vs. sparse rural) without modifying the core decision algorithm in [`RelayController.swift`](https://github.com/permissionlesstech/bitchat/blob/main/RelayController.swift).