Couchbase Lite

Zero-Latency Quizzing: How P2P Replication Solved the Conference Wi-Fi Problem

9 MIN DE LEITURA

If you have ever hosted an interactive quiz with a large crowd in a single conference room, you know how quickly local Wi-Fi can bottleneck. When dozens of devices simultaneously fight for bandwidth to connect to remote servers, even the best platforms can get bogged down by network congestion, leading to frustrating latency and loading spinners.

This connectivity challenge is a common hurdle at developer meetups and workshops. The irony is hard to miss: when participants are sitting just a few feet apart in the same room, data shouldn’t have to travel out to a distant cloud server and back just to sync a question across a local group of screens.

So I built KahootP2P. It is a Kahoot-style quiz game for iOS that runs entirely over local peer-to-peer networking. No internet. No router. No cloud. Just devices that talk directly to each other, powered by Couchbase Lite Enterprise’s P2P replication.

This post shares the motivation behind the project, the architecture decisions involved, and how Couchbase Lite makes the hardest parts surprisingly simple.

The Problem With Cloud-Dependent Multiplayer

Kahoot is brilliant. The speed-based scoring creates real tension, the simple four-color answer grid works on any screen size, and the leaderboard after each question keeps everyone engaged. But it has a fundamental dependency: every single interaction goes through the internet.

For a classroom with dedicated Wi-Fi, a cloud-reliant approach works perfectly. However, in a packed conference hall where 200 people are sharing the same network, that centralized connection model can quickly lead to severe latency and connectivity drops.

Latency also matters. Kahoot awards more points for faster answers. When your correct answer takes 800ms to reach the server instead of 200ms because of network congestion, you are literally losing points through no fault of your own. That is not a great experience.

The goal, therefore, was to achieve network latency measured in single-digit milliseconds, not hundreds. The only way to get there was to cute out the internet entirely.

Why Couchbase Lite P2P?

In terms of offline multiplayer experience, Apple’s MultipeerConnectivity framework handles discovery and basic message passing between iOS devices. But there’s a catch: it is designed  for mesh topologies where every device talks to every other device. For a quiz game with a host that controls the flow, this creates challenges around consistency and ordering.

The most important parameters include:

  • A single source of truth – The host device should be the authority on scores, question timing, and game state
  • Automatic data sync – When the host writes a new question or updates scores, every player should see it without me manually serializing and routing messages
  • Reliable delivery – If a player’s answer takes a moment to sync, it should not get lost

Couchbase Lite Enterprise’s peer-to-peer replication fits this perfectly. 

The URLEndpointListener lets one device (the host) act as a replication target. Player devices connect to it using a standard Couchbase Lite replicator in push-and-pull mode. This gives a star topology with the host at the center – exactly what a host-authoritative game needs.

The key insight is that there is no need to build a custom message protocol at all. Instead of thinking in terms of “send questions to all players” and “receive answers from player 3,” documents are simply written to the local database, while the replication layer handles delivering them to the right places.

Architecture: Documents as Game State

The entire game state lives in Couchbase Lite documents. Here’s how it is modeled:Game – A single document that tracks the game title, join code, phase (lobby/asking/closed/finished), and question count. When the host changes the phase, replication pushes it to all players.

Game – A single document that tracks the game title, join code, phase (lobby/asking/closed/finished), and question count. When the host changes the phase, replication pushes it to all players.

class Game: Codable, Identifiable {
    @DocumentID var id: String?
    var title: String
    var joinCode: String
    var phase: GamePhase
    var questionCount: Int
    var createdAt: Date
}
func sta

QuestionState – One document that tells every player what question they are on and whether it is still open for answers. The host updates this document to advance the game.

Questions – Individual documents for each question, synced from host to players when the game starts.

Answers – Each player writes their answer as a document. The replication pushes it to the host for scoring.

AnswerResults – After scoring, the host writes result documents back, which replicate to the relevant player.

Leaderboard –  A single document with ranked entries, updated by the host after each question.

The beauty of this approach is that the sync is bidirectional and automatic. Players push answers up, the host pushes results down, and Couchbase Lite handles all the conflict resolution and ordering.

Discovery: Finding Games Without a Server

Before devices can replicate data, they need to find each other. Bonjour (Apple’s zero-configuration networking protocol) is used for this.

When the host starts a game, two things happen:

  1. A URLEndpointListener starts on an available port.
  2. A Bonjour service is published advertising that port.
rtHosting(gameId: String) throws {
    var listenerConfig = URLEndpointListenerConfiguration(
        collections: [collection]
    )
    listenerConfig.port = 0  // Let the OS pick a port
    listenerConfig.disableTLS = true

    let urlListener = URLEndpointListener(config: listenerConfig)
    try urlListener.start()

    // Advertise via Bonjour so players can find us
    let service = NetService(
        domain: "",
        type: "_quizblitz._tcp.",
        name: "QuizBlitz-\(gameId.prefix(8))",
        port: Int32(urlListener.port ?? 0)
    )
    service.publish()
}

On the player side, a NetServiceBrowser scans for the _quizblitz._tcp. service type. When it finds a game, the player taps to join, and a Couchbase Lite replicator connects to the host’s URLEndpointListener:

func connectToHost(host: String, port: Int) {
    let url = URL(string: "ws://\(host):\(port)/\(db.name)")!
    let targetEndpoint = URLEndpoint(url: url)

    var config = ReplicatorConfiguration(
        collections: [colConfig],
        target: targetEndpoint
    )
    config.replicatorType = .pushAndPull
    config.continuous = true

    let repl = Replicator(config: config)
    repl.start()
}

That’s it. Once the replicator is running, documents flow automatically in both directions. No manual message handling.

Scoring: Speed Matters

Like Kahoot, faster correct answers earn more points. But here is the tricky part – in a P2P setup, you can’t simply use wall-clock timestamps because devices might not have synchronized clocks (and without the internet, there is no NTP server to sync with).

This solution uses the host’s monotonic clock (via mach_continuous_time) as the single timing reference. When the host opens a question, it records its own uptime in nanoseconds. When an answer document replicates to the host, the host records the receipt time.

let timeDelta = TimingService.elapsedSeconds(
    from: startNs, to: receivedNs
)
let timeBonus = max(0, 1.0 - (timeDelta / Double(timeLimitSeconds)))
let points = isCorrect ? max(100, Int(1000.0 * timeBonus)) : 0

Is this perfectly fair? No. The replication latency adds a few milliseconds of noise. But in practice, over local Wi-Fi, the replication lag is under 50ms. For a game where the time limit is 15 seconds, that delay is negligible. The person who knows the answer and taps faster will still win.

The scoring works on a sliding scale: answer instantly and get 1000 points. Answer at the very last second and get 100 points. Answer wrong and get zero. This creates the same urgency you feel in Kahoot.

Reactive UI with Combine

The UI needs to update in real time as documents change. Couchbase Lite’s Combine integration makes this clean:

db.collectionChangePublisher()
    .receive(on: DispatchQueue.main)
    .sink { [weak self] change in
        self?.handleCollectionChange(change)
    }
    .store(in: &cancellables)

When any document in the collection changes –  whether from local writes or incoming replication – this publisher fires. The handler checks if the change is relevant (same game ID) and updates the appropriate state: new player joined, answer received, leaderboard updated.

On the SwiftUI side, the engines are ObservableObject classes with @Published properties. When the host writes a new QuestionState document and it replicates to a player, the player’s ClientEngine picks up the change through the collection publisher, updates its @Published properties, and SwiftUI re-renders the view. The whole chain from “host presses Next Question” to “player sees the new question” takes under 100ms on a local network.

The Codable Integration

One of the things that made development faster was Couchbase Lite’s Codable support. Instead of manually mapping between documents and Swift objects, models could simply be defined as as Codable classes:

// Save a model directly
try collection.save(from: game)

// Load it back as a typed object
let game = try collection.document(id: docId, as: Game.self)

The @DocumentID property wrapper automatically maps the Couchbase Lite document ID to a property on the model. This makes it possible to work with normal Swift objects everywhere and lets the SDK handle serialization automatically.

For queries, I used SQL++:

let sql = "SELECT META().id, * FROM _ WHERE gameId = '\(gameId)' AND displayName IS NOT MISSING"
let query = try database.createQuery(sql)

Key Takeaways

Documents as game state work surprisingly well. There was initial skepticism about using a database replication protocol for real-time game sync. In practice, the latency is low enough that it feels instant, and you get reliable delivery, conflict resolution, and persistence for free.

Star topology is the right call for authoritative games. A mesh network sounds cool, but when one device needs to be the authority on scoring and game flow, a hub-and-spoke model with the host at the center is simpler and more predictable.

You do not need the internet for most local multiplayer. This might seem obvious, but it is easy to default to “spin up a server” when the devices you want to connect are literally in the same room. Couchbase Lite’s P2P replication eliminates that entire dependency.

Bonjour discovery just works. Bonjour on iOS is rock solid. Devices find each other within 1-2 seconds consistently.

Try It

The full source code is on GitHub: github.com/midopooler/KahootP2P

You will need Couchbase Lite Enterprise (for the URLEndpointListener) and two or more iOS devices on the same local network. Simply clone the repo, run xcodegen generate, build, and you are playing within a minute.

If you are building any kind of local multiplayer, collaborative, or offline-first experience on mobile, Couchbase Lite’s P2P capabilities are worth considering. It eliminates the need to write a single line of networking code beyond “start the listener” and “start the replicator,” which saves a significant amount of time.

The game is simple, but the pattern applies much more than quizzes, including field data collection with team sync, collaborative note-taking in airplane mode, and point-of-sale systems that keep working when the internet drops. Any time you need multiple devices to share state without depending on the cloud, this same architecture works.

Share this article

Author

Deixe um comentário

Ready to get Started with Couchbase Capella?

Start building

Check out our developer portal to explore NoSQL, browse resources, and get started with tutorials.

Use Capella free

Get hands-on with Couchbase in just a few clicks. Capella DBaaS is the easiest and fastest way to get started.

Get in touch

Want to learn more about Couchbase offerings? Let us help.