This is an excerpt. The full article includes a live interactive network ARQ simulator — adjust packet loss rates and Round Trip Times (RTT) in real time, choose between Selective Repeat and Go-Back-N, transmit message frames, and watch the sliding window resend dropped packets. Read the full interactive version →
The Network Dilemma: TCP vs. UDP
When designing network architectures, we traditionally choose between two standard transport layer protocols:
- TCP (Transmission Control Protocol): Guarantees in-order, error-free delivery. However, it suffers from heavy connection handshake overhead, head-of-line blocking (where a single dropped packet stalls all subsequent packets), and rigid congestion control algorithms.
- UDP (User Datagram Protocol): A lightweight, connectionless protocol with minimal packet overhead. It sends packets immediately without waiting, but it does not guarantee delivery, packet order, or duplicate prevention.
For real-time, high-throughput systems (such as multiplayer game servers, streaming platforms, or WebRTC data sync channels), we need both the speed of UDP and the reliability of TCP.
Reliable UDP (RUDP) solves this. By implementing reliability mechanisms in user space on top of raw UDP sockets, developers can choose which packets require guaranteed delivery, customize timeout behaviors, and avoid head-of-line blocking.
1. Custom Packet Header Structures
To enforce reliability over a connectionless UDP socket, we must append a custom protocol header to every payload.
This custom header typically contains the following fields:
- Sequence Number (32 bits): Identifies the exact order of the packet so the receiver can reassemble them correctly.
- Acknowledgment Number (32 bits): Confirms the sequence numbers of successfully received packets.
-
Flags (8 bits): Controls packet types, such as connection setup (
SYN), data payloads (DAT), or confirmations (ACK/NACK). - Checksum (16 bits): Validates packet integrity to detect data corruption during transit.
┌───────────────────────────────────────────────────────────────┐
│ CUSTOM RUDP PACKET │
├───────────────────────┬───────────────────────┬───────────────┤
│ Sequence Number (32b) │ ACK Number (32b) │ Flags (8b) │
├───────────────────────┴───────────────────────┴───────────────┤
│ Data Payload (Variable) │
└───────────────────────────────────────────────────────────────┘
2. Sliding Window Flow Control
Sending one packet and waiting for its ACK before sending the next (Stop-and-Wait) is very slow. To maximize bandwidth, we use a Sliding Window.
The sender maintains a "window" of allowed, unacknowledged packets (e.g., sequence numbers 0 to 3). As long as the window isn't full, the sender transmits packets continuously.
When the receiver ACKs the oldest packet in the window (the base), the window slides forward, allowing new packets to be sent. This keeps the network link saturated and increases overall data throughput.
[ACKed] [Sent, UnACKed] [Unsent]
┌───────┐ ┌───────────────┐ ┌─────────┐
... │ 0 1 │ │ 2 3 4 5 │ │ 6 7 │ ...
└───────┘ └───────────────┘ └─────────┘
▲ ▲
│ │
Window Base Next Seq Num
3. ARQ Error Recovery: GBN vs. SR
When a packet is lost in transit, the protocol must decide how to recover. There are two main strategies:
Go-Back-N (GBN)
In Go-Back-N, the receiver only accepts packets in strict, sequential order. If packet 2 is lost, the receiver discards all subsequent packets (3, 4, 5), even if they arrive safely.
The sender's timeout is triggered for packet 2, and the sender must retransmit packet 2 and all following packets in the window (2, 3, 4, 5). This is simple to implement but wastes bandwidth on lossy networks.
Selective Repeat (SR)
In Selective Repeat, the receiver accepts out-of-order packets and buffers them. If packet 2 is lost but 3, 4, and 5 arrive, the receiver keeps them and sends ACKs for them.
The sender detects that only packet 2 is missing and retransmits only packet 2. Once packet 2 arrives, the receiver merges it with the buffered packets and delivers them in order to the application, minimizing retransmission traffic.
TypeScript RUDP Sender Implementation
Here is a clean, modular TypeScript implementation mapping sliding window boundaries, Selective Repeat timers, and packet serialization:
export interface UDPPacket {
seq: number;
flags: { SYN: boolean; ACK: boolean; DAT: boolean };
checksum: number;
payload: string;
}
export class SlidingWindowSender {
private windowSize: number = 4;
private nextSeqNum: number = 0;
private base: number = 0;
private sendBuffer: Map<number, UDPPacket> = new Map();
private ackedPackets: Set<number> = new Set();
private timers: Map<number, NodeJS.Timeout> = new Map();
private readonly TIMEOUT_MS = 1500;
constructor(private socketSend: (packet: UDPPacket) => void) {}
/**
* Appends payload to buffer and attempts transmission.
*/
public send(payload: string): void {
const packet: UDPPacket = {
seq: this.nextSeqNum,
flags: { SYN: false, ACK: false, DAT: true },
checksum: this.calculateChecksum(payload),
payload
};
this.sendBuffer.set(this.nextSeqNum, packet);
this.nextSeqNum++;
this.tryTransmit();
}
private tryTransmit(): void {
// Transmit packets falling within sliding window limits
while (this.nextSeqNum < this.base + this.windowSize && this.sendBuffer.has(this.nextSeqNum)) {
const packet = this.sendBuffer.get(this.nextSeqNum)!;
this.transmitPacket(packet);
}
}
private transmitPacket(packet: UDPPacket): void {
this.socketSend(packet);
this.startTimer(packet.seq);
}
private startTimer(seq: number): void {
if (this.timers.has(seq)) return;
const timer = setTimeout(() => {
// Timeout triggered: retransmit packet
const packet = this.sendBuffer.get(seq);
if (packet && !this.ackedPackets.has(seq)) {
this.transmitPacket(packet);
}
}, this.TIMEOUT_MS);
this.timers.set(seq, timer);
}
/**
* Receiver notifies sender of an Acknowledgment (ACK)
*/
public handleAck(ackSeq: number): void {
this.ackedPackets.add(ackSeq);
// Stop timer
if (this.timers.has(ackSeq)) {
clearTimeout(this.timers.get(ackSeq)!);
this.timers.delete(ackSeq);
}
// Slide window base forward if the oldest packet in the window was ACKed
if (ackSeq === this.base) {
while (this.ackedPackets.has(this.base)) {
this.base++;
}
this.tryTransmit();
}
}
private calculateChecksum(data: string): number {
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum += data.charCodeAt(i);
}
return sum % 65535;
}
}
Engineering Takeaways
- RUDP is the foundation of modern web protocols: Technologies like QUIC (which powers HTTP/3) and WebRTC are built directly on top of UDP to avoid TCP handshakes and head-of-line blocking.
- Selective Repeat is essential on lossy networks: While Go-Back-N is simpler, it wastes massive amounts of bandwidth under heavy packet loss by retransmitting valid data.
- User-space implementation enables flexibility: Moving transport-layer logic out of the OS kernel allows developers to optimize congestion controls for specific applications.
The full article features a live 2D network packet simulator — adjust network loss and latency sliders, select Selective Repeat or Go-Back-N, and watch sliding window frames, packet drops, and ACK flows resolve in real time.
Written by Ebenezer Akinseinde — Software Developer & AI Automations Engineer.
Top comments (1)
Really like that you actually implemented selective repeat instead of just describing it. One thing that might be off in SlidingWindowSender though: send() bumps nextSeqNum right after buffering the packet, then tryTransmit() loops while sendBuffer.has(this.nextSeqNum), which is the slot you just moved past, so the very first packet never seems to leave. And if it did enter the loop, nothing inside advances nextSeqNum, so it would resend the same packet forever. Feels like you need a separate pointer for "next sequence to assign" versus "next to transmit", with tryTransmit walking the second one. Or am I misreading how the buffer is keyed?