# What Is the Gossip Sync Protocol? How Bitchat Synchronizes Messages in the Mesh

> Discover the gossip sync protocol's bandwidth-efficient approach to mesh message synchronization. Learn how Golomb-Coded Set filters ensure eventual consistency without central servers.

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

---

**The gossip sync protocol is a bandwidth-efficient, probabilistic synchronization mechanism that uses Golomb-Coded Set (GCS) filters to exchange missing broadcast packets between peers, ensuring eventual consistency across the mesh without requiring centralized servers.**

The gossip sync protocol powers decentralized message propagation in Bitchat, an open-source, permissionless mesh messaging application developed by permissionlesstech. By combining periodic maintenance cycles with compact probabilistic filters, the protocol enables nodes to discover and exchange missing messages efficiently while bounding bandwidth and storage usage. This article examines the protocol's architecture and implementation based on the Bitchat source code.

## Core Architecture and Components

The protocol centers on the **`GossipSyncManager`** class defined in [`bitchat/Sync/GossipSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift). This manager orchestrates packet storage, filter construction, and synchronization scheduling across the mesh network.

### Packet Stores and LRU Caching

The manager maintains dedicated **`PacketStore`** instances for each broadcast type—messages, fragments, file transfers, group messages, pre-key bundles, announces, and board posts. These stores function as bounded LRU caches that retain only the freshest entries according to configurable capacity limits.

When a broadcast packet arrives, the manager verifies its **`MessageType`** and freshness via `isPacketFresh`, which compares the packet timestamp against `maxMessageAgeSeconds`. Valid packets are inserted using deterministic IDs generated by `PacketIdUtil.computeId`:

```swift
func _onPublicPacketSeen(_ packet: BitchatPacket) {
    guard let messageType = MessageType(rawValue: packet.type) else { return }
    switch messageType {
    case .message:
        guard isBroadcastRecipient && isPacketFresh(packet) else { return }
        let idHex = PacketIdUtil.computeId(packet).hexEncodedString()
        messages.insert(idHex: idHex, packet: packet, capacity: max(1, config.seenCapacity))
        archiveDirty = true
    // ... similar handling for .fragment, .fileTransfer, .groupMessage, etc.
    }
}

```

### Golomb-Coded Set (GCS) Filters

The protocol uses **GCS filters** to create space-efficient, probabilistic representations of packet ID sets. During each sync round, the manager constructs a filter fitting within `gcsMaxBytes` that encodes the set of packet IDs known to the node. The filter parameters—**`p`**, **`m`**, and **`data`**—are transmitted in `REQUEST_SYNC` payloads to minimize bandwidth consumption compared to sending full ID lists.

## The Synchronization Lifecycle

### Periodic Maintenance and Scheduling

Every `maintenanceIntervalSeconds`, the manager executes `performPeriodicMaintenance`, which iterates over **`syncSchedules`**—one schedule per packet-type group. For each due schedule, the manager calls `sendPeriodicSync`, dispatching either a unicast to known peers or a broadcast request:

```swift
private func performPeriodicMaintenance(now: Date = Date()) {
    // ... cleanup logic omitted
    for index in syncSchedules.indices {
        guard syncSchedules[index].interval > 0 else { continue }
        if syncSchedules[index].lastSent == .distantPast ||
           now.timeIntervalSince(syncSchedules[index].lastSent) >= syncSchedules[index].interval {
            syncSchedules[index].lastSent = now
            sendPeriodicSync(for: syncSchedules[index].types)
        }
    }
}

```

### Building the REQUEST_SYNC Payload

The `buildGcsPayload(for:types)` method gathers candidate packets, sorts them newest-first, and constructs the filter. If the store exceeds the byte budget, only the most recent `takeN` packets are included, and the manager records a **`sinceTimestamp`** cursor representing the oldest included packet. This cursor allows responders to skip older packets that the requester already possesses:

```swift
private func buildGcsPayload(for types: SyncTypeFlags, fragmentIdFilter: String? = nil) -> Data {
    var candidates: [BitchatPacket] = []
    if types.contains(.message) { 
        candidates.append(contentsOf: messages.allPackets(isFresh: isPacketFresh)) 
    }
    // ... add other types
    candidates.sort { $0.timestamp > $1.timestamp }
    let ids = candidates.prefix(takeN).map { PacketIdUtil.computeId($0) }
    let params = GCSFilter.buildFilter(ids: ids, maxBytes: config.gcsMaxBytes, targetFpr: config.gcsTargetFpr)
    let sinceTimestamp = // derived from params.includedCount
    let req = RequestSyncPacket(p: params.p, m: params.m, data: params.data,
                                types: types, sinceTimestamp: sinceTimestamp,
                                fragmentIdFilter: fragmentIdFilter)
    return req.encode()
}

```

### Handling Inbound Sync Requests

When a peer transmits a `REQUEST_SYNC`, the manager processes it via `_handleRequestSync`. The implementation first checks **`SyncResponseRateLimiter`** to prevent DoS attacks that could force expensive full-store diffs. It then decodes the peer's GCS filter and iterates local packets, using `mightContain` to identify missing items. For each missing packet, the manager sends a solicited response marked with **`isRSR = true`** and **`ttl = 0`** (local-only propagation):

```swift
private func _handleRequestSync(from peerID: PeerID, request: RequestSyncPacket) {
    guard responseRateLimiter.shouldRespond(to: peerID, now: Date()) else { return }
    let sorted = GCSFilter.decodeToSortedSet(p: request.p, m: request.m, data: request.data)
    func mightContain(_ id: Data) -> Bool { /* ... */ }
    if request.types.contains(.message) {
        for pkt in messages.allPackets(isFresh: isPacketFresh) where pkt.timestamp >= request.sinceTimestamp {
            if !mightContain(PacketIdUtil.computeId(pkt)) {
                var toSend = pkt; toSend.ttl = 0; toSend.isRSR = true
                delegate?.sendPacket(to: peerID, packet: toSend)
            }
        }
    }
    // ... similar blocks for other types
}

```

Notably, announces and pre-key bundles are exempt from the `sinceTimestamp` cursor because they are rare and bounded, ensuring new peers can always obtain critical cryptographic material.

## Persistence and Hygiene Mechanisms

### GossipMessageArchive

Public messages persist to disk via **`GossipMessageArchive`** ([`bitchat/Sync/GossipMessageArchive.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipMessageArchive.swift)), enabling the app to resume gossip sync with a partially-filled store after restart. The manager calls `persistNow` before backgrounding and `restoreArchivedMessages` during initialization.

### Rate Limiting and Cleanup

The **`SyncResponseRateLimiter`** ([`bitchat/Sync/SyncResponseRateLimiter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/SyncResponseRateLimiter.swift)) restricts how frequently a specific peer can trigger synchronization responses. Additionally, `cleanupStaleAnnouncements` prunes expired peer announcements after `stalePeerTimeoutSeconds`, and the manager supports removing specific peers' messages (e.g., on block) or wiping the entire archive when the user clears the timeline.

## Integration with the Network Stack

The **BLEService** ([`bitchat/Services/BLE/BLEService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/BLE/BLEService.swift)) instantiates the `GossipSyncManager`, forwards inbound packets via `onPublicPacketSeen`, and invokes `scheduleInitialSyncToPeer` when new peers connect. The **RequestSyncManager** tracks outbound requests to protect against replay attacks, while **NoiseEncryptionService** consumes gossiped pre-key bundles for end-to-end encryption setup.

```swift
// Typical integration pattern within BLEService
let syncMgr = GossipSyncManager(
    myPeerID: myPeerID,
    requestSyncManager: requestSyncManager,
    archive: GossipMessageArchive(path: archivePath)
)
syncMgr.delegate = self
syncMgr.start()

// Handle incoming mesh traffic
func handleIncoming(_ pkt: BitchatPacket) {
    syncMgr.onPublicPacketSeen(pkt)
}

// Trigger sync with newly discovered peers
func peerDidConnect(_ peerID: PeerID) {
    syncMgr.scheduleInitialSyncToPeer(peerID, delaySeconds: 3.0)
}

```

## Summary

- **GossipSyncManager** orchestrates mesh-wide synchronization from [`bitchat/Sync/GossipSyncManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/GossipSyncManager.swift), managing per-type LRU caches and periodic maintenance cycles.
- **GCS filters** provide compact, probabilistic membership testing via parameters `p`, `m`, and `data`, significantly reducing sync bandwidth compared to full ID enumeration.
- **REQUEST_SYNC** packets carry filter data and `sinceTimestamp` cursors to enable incremental synchronization, while rare packets like pre-key bundles bypass temporal filtering.
- **SyncResponseRateLimiter** prevents resource exhaustion by throttling expensive diff operations from aggressive peers.
- **GossipMessageArchive** ensures gossip state survives application restarts, restoring the packet store from disk on launch.

## Frequently Asked Questions

### How does the gossip sync protocol handle stale or expired messages?

The protocol validates packet freshness via `isPacketFresh` before insertion into the `PacketStore`, rejecting packets older than `maxMessageAgeSeconds` (with extended windows for public messages). During sync responses, the `sinceTimestamp` cursor ensures peers only evaluate messages newer than the filter's oldest entry, while the LRU cache automatically evicts aged entries to bound memory usage.

### What is a GCS filter and why does Bitchat use it for synchronization?

A **GCS (Golomb-Coded Set) filter** is a space-efficient probabilistic data structure that tests set membership with a configurable false-positive rate. Bitchat encodes known packet IDs into these filters using `GCSFilter.buildFilter` to fit within `gcsMaxBytes`, allowing peers to compare state using kilobytes rather than megabytes of raw ID lists, which is critical for bandwidth-constrained mesh networks.

### How does the protocol prevent malicious peers from overwhelming the network with sync requests?

The **`SyncResponseRateLimiter`** ([`bitchat/Sync/SyncResponseRateLimiter.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Sync/SyncResponseRateLimiter.swift)) tracks request frequency per peer ID via `shouldRespond`. If a peer attempts to trigger full-store diffs too frequently, the manager drops the request, preventing computational denial-of-service attacks that could exhaust battery or bandwidth on mobile mesh nodes.

### What happens to gossip state when the Bitchat app restarts?

The **`GossipMessageArchive`** persists recent public messages to disk before the application backgrounds. On launch, `restoreArchivedMessages` repopulates the `PacketStore` instances, allowing the node to resume gossip sync immediately without re-downloading messages it already possesses, ensuring continuity in the mesh's eventual consistency model.