# How DD Poker Handles Network Latency and Synchronization in Online Play

> Discover how DD Poker masters network latency and synchronization for online play using custom UDP transport, adaptive timers, and MTU discovery. Ensure seamless distributed tables despite variable conditions.

- Repository: [Doug Donohoe/ddpoker](https://github.com/dougdonohoe/ddpoker)
- Tags: architecture
- Published: 2026-02-28

---

**DD Poker implements a custom UDP transport layer with periodic heartbeat pings, adaptive retransmission timers, and session-based MTU discovery to maintain tight synchronization across distributed poker tables despite variable network conditions.**

The open-source DD Poker application (repository `dougdonohoe/ddpoker`) replaces TCP with a lightweight UDP networking stack to minimize protocol overhead, then rebuilds reliability and **network latency and synchronization** guarantees directly in application code. This design provides millisecond-level control over packet recovery, congestion adaptation, and session continuity required for real-time card games.

## The Three-Pillar Architecture for Low-Latency Sync

The networking subsystem centers on three complementary mechanisms implemented in [`code/udp/src/main/java/com/donohoedigital/udp/UDPLink.java`](https://github.com/dougdonohoe/ddpoker/blob/main/code/udp/src/main/java/com/donohoedigital/udp/UDPLink.java):

- **Periodic Ping/Ack Heartbeat**: Every approximately 333 ms, `UDPManager` schedules a `SendAckTask` that transmits either a normal ACK batch or a "ping-ack" packet (`SENDALL` versus `SENDACK`). The round-trip time of these acknowledgments is recorded in `UDPLink.UDPStats` via `recordRoundTripTime`, producing a moving-average latency estimate exposed through `stats.getAverage()`.

- **Adaptive Retransmission**: When a queued `UDPData` fragment remains unacknowledged longer than a dynamic threshold—calculated as `minAck = max(1000, 1.25 × averageRTT)` milliseconds but never exceeding 7 seconds—the link automatically resends it. The system permits up to 25 retry attempts for normal game data and 3 attempts for MTU discovery probes, ensuring eventual delivery without flooding the connection.

- **Session and MTU Discovery**: Each connection maintains unique `localSessionID_` and `remoteSessionID_` values. When a new session begins, all queues clear and the link executes a `HELLO`→`ACK` handshake. Concurrently, `mtuPathDiscovery()` (lines 580‑617) probes the network path to find the largest unfragmented payload size, preventing hidden latency spikes caused by IP fragmentation.

## Core Implementation: From Link Creation to Data Flow

The synchronization logic follows a strict lifecycle coordinated by [`UDPManager.java`](https://github.com/dougdonohoe/ddpoker/blob/main/UDPManager.java) and executed within [`UDPLink.java`](https://github.com/dougdonohoe/ddpoker/blob/main/UDPLink.java).

### Link Establishment and Hello Handshake

When a client joins a table, `UDPManager.getLink()` (lines 28‑45) either returns an existing `UDPLink` or instantiates a fresh instance and adds it to the internal `links_` collection. The link immediately enters a handshake phase:

```java
// Acquire a link to the remote host (IP + port)
UDPLink link = manager.getLink("203.0.113.42", 7777);

// Kick‑off the hello handshake; this also starts MTU discovery
link.connect();   // calls hello() → mtuPathDiscovery() → send()

```

The `hello()` method (lines 514‑523) transmits a `HELLO` message; upon receiving the corresponding `ACK` processed at lines 1490‑1498, the link fires a `UDPLinkEvent.Type.ESTABLISHED` event and begins accepting `MESSAGE` packets.

### Continuous Latency Monitoring

Once established, the link maintains a bi-directional heartbeat via `SendAckTask`. Every third tick (~1 second) the task queues `SENDALL` to flush pending data; on other ticks it queues `SENDACK`. The `sendAcksPing()` method (lines 698‑706) first verifies the connection is alive via `aliveCheck()` before transmitting.

When the remote peer returns an ACK, `processAcks` invokes `stats_.recordRoundTripTime(qData)` (lines 1114‑1117). The `UDPStats` class maintains a moving average accessible via `getAverage()` (lines 727‑734), providing a live millisecond-accurate latency reading.

### Intelligent Packet Recovery

The `sendAll()` method (lines 221‑260) iterates over the outbound queue and resends any `UDPData` whose age exceeds the adaptive `minAck` threshold. The resend loop (lines 369‑390) increments attempt counters and discards fragments that exceed the maximum retry limit (25 for data, 3 for MTU tests). This balances responsiveness against network congestion.

### Timeout Handling and Session Reset

If `aliveCheck()` (lines 666‑689) detects no traffic—including pings—for longer than the configurable `TIMEOUT_MILLIS`, the link emits a `TIMEOUT` event, invokes `resetSession()` (lines 1068‑1074), and forces a new `HELLO` handshake. This rapidly recovers from prolonged latency spikes or temporary network partitions.

## Session Management and MTU Discovery

Session identifiers prevent stale packets from corrupting game state after a reconnect. When `resetSession()` clears the `localSessionID_`, the next outbound `HELLO` establishes a fresh logical connection, and the remote peer updates its `remoteSessionID_` accordingly.

Before normal gameplay resumes, `mtuPathDiscovery()` transmits a series of payloads with increasing sizes. Successful `MTU_ACK` responses (handled at lines 1025‑1035) raise the `nMTU_` value until the path limit is found. By avoiding IP fragmentation, the system prevents "phantom" latency caused by fragmented packet reassembly or loss.

## Measuring and Displaying Network Health

The UI layer consumes the same statistics used by the transport layer. In [`code/pokerwicket/src/main/java/com/donohoedigital/games/poker/wicket/pages/support/OnlineSupplement.html`](https://github.com/dougdonohoe/ddpoker/blob/main/code/pokerwicket/src/main/java/com/donohoedigital/games/poker/wicket/pages/support/OnlineSupplement.html), the application displays latency and packet loss to the player.

Retrieve the current latency programmatically:

```java
// Periodically (e.g., every second) read the moving‑average RTT
long latencyMs = link.getStats().getAverage();   // in milliseconds
System.out.println("Current network latency: " + latencyMs + " ms");

```

Event listeners monitor connection state changes to trigger visual warnings:

```java
link.addMonitor(event -> {
    switch (event.getType()) {
        case TIMEOUT:
            System.out.println("Link timed out – will re‑handshake");
            // UI can show a “re‑connecting…” banner
            break;
        case SESSION_CHANGED:
            System.out.println("Remote session changed – resetting state");
            // Flush any pending game actions that depend on the old session
            break;
        default:
            // handle other events (ESTABLISHED, RESEND_FAILURE, etc.)
    }
});

```

Display the value in a Wicket component:

```java
add(new Label("latency", new LoadableDetachableModel<String>() {
    @Override
    protected String load() {
        UDPLink link = getSessionLink();   // application‑specific lookup
        return link != null ? link.getStats().getAverage() + " ms" : "—";
    }
}).setOutputMarkupId(true));

```

## Summary

- **Custom UDP Transport**: DD Poker bypasses TCP overhead with a lightweight UDP layer implemented in [`UDPManager.java`](https://github.com/dougdonohoe/ddpoker/blob/main/UDPManager.java) and [`UDPLink.java`](https://github.com/dougdonohoe/ddpoker/blob/main/UDPLink.java), rebuilding reliability in user space.
- **Adaptive Timing**: Latency-aware retransmission uses a moving-average RTT multiplied by 1.25 (minimum 1 s, maximum 7 s) to determine when to resend lost packets.
- **Session Isolation**: Unique session IDs ensure that delayed packets from previous connections cannot corrupt the current game state after a reconnect.
- **Path MTU Discovery**: Proactive MTU testing prevents fragmentation-related latency spikes by locking the payload size to the network path's actual capacity.
- **Observability**: Real-time latency statistics flow from `UDPStats` directly to the Wicket-based UI, giving players visibility into connection health.

## Frequently Asked Questions

### How does DD Poker measure network latency during gameplay?

The transport layer records round-trip times for every acknowledgment packet. In `UDPLink.processAcks()`, the code invokes `stats_.recordRoundTripTime()` to update a moving-average statistic. The UI retrieves this value via `link.getStats().getAverage()` and displays it in the Online Support page, giving players a live latency reading updated approximately every second.

### What happens when a packet is lost or delayed?

If a `UDPData` fragment remains unacknowledged beyond the adaptive threshold—calculated as `max(1000ms, 1.25 × averageRTT)` capped at 7 seconds—the `sendAll()` method automatically retransmits it. The system allows up to 25 retry attempts for game messages before logging a `RESEND_FAILURE` event, ensuring critical state changes eventually reach all players.

### Why does DD Poker use UDP instead of TCP for online play?

TCP's congestion control and head-of-line blocking can introduce unpredictable latency spikes that disrupt real-time card game synchronization. By implementing a custom UDP-based protocol in `code/udp/src/main/java/com/donohoedigital/udp/`, DD Poker gains fine-grained control over retransmission timing, session resets, and MTU sizing, allowing it to maintain tight synchronization even on lossy or high-latency internet connections.

### How does the game recover from a complete network disconnection?

When no packets arrive for the duration specified by `TIMEOUT_MILLIS`, the `aliveCheck()` method triggers a `TIMEOUT` event and calls `resetSession()`. This clears all pending queues, generates new session IDs, and initiates a fresh `HELLO` handshake. Consequently, the client can rapidly rejoin the game table without requiring a full application restart, preserving the player's seat and chip stack.