UDP’s inherent unreliability, the very thing that makes it fast, is also its biggest weakness.

Imagine a network stream of video or voice. Packets arrive, or they don’t. If they don’t, the video stutters, the voice drops out. This is UDP. Now, what if we want the speed of UDP but need to ensure every packet gets there, or at least know when it doesn’t? That’s where a UDP reliability layer comes in, typically implemented by adding retransmission and acknowledgment mechanisms on top of UDP.

Let’s see this in action with a simplified conceptual example. We’ll use two Go programs: a sender and a receiver.

Sender (sender.go)

package main

import (
	"fmt"
	"net"
	"time"
)

type Packet struct {
	SeqNum uint32
	Data   []byte
}

func main() {
	addr, _ := net.ResolveUDPAddr("udp", ":8080")
	conn, _ := net.DialUDP("udp", nil, addr)
	defer conn.Close()

	seqNum := uint32(0)
	buffer := make([]byte, 1024)

	for i := 0; i < 100; i++ {
		// Prepare packet
		packet := Packet{SeqNum: seqNum, Data: []byte(fmt.Sprintf("message %d", seqNum))}

		// Serialize packet (simplified for example)
		packetBytes := make([]byte, 4+len(packet.Data))
		copy(packetBytes, []byte{0, 0, 0, 0}) // Placeholder for seqNum
		copy(packetBytes[4:], packet.Data)
		// In a real scenario, you'd encode seqNum properly.

		// Send packet
		_, err := conn.Write(packetBytes)
		if err != nil {
			fmt.Printf("Error sending packet %d: %v\n", seqNum, err)
			continue
		}
		fmt.Printf("Sent packet %d\n", seqNum)

		// Wait for ACK (simplified: assume ACK for this seqNum)
		// In a real implementation, this would involve a separate receive loop
		// and matching ACKs to sent seqNums.
		time.Sleep(50 * time.Millisecond) // Simulate network latency and processing
		seqNum++
	}
}

Receiver (receiver.go)

package main

import (
	"fmt"
	"net"
	"time"
)

type Packet struct {
	SeqNum uint32
	Data   []byte
}

func main() {
	addr, _ := net.ResolveUDPAddr("udp", ":8080")
	conn, _ := net.ListenUDP("udp", addr)
	defer conn.Close()

	buffer := make([]byte, 1024)
	lastAckedSeqNum := uint32(0) // Track the last acknowledged sequence number

	for {
		n, _, err := conn.ReadFromUDP(buffer)
		if err != nil {
			fmt.Printf("Error reading from UDP: %v\n", err)
			continue
		}

		// Deserialize packet (simplified)
		seqNum := uint32(0) // Placeholder
		data := buffer[:n-4]
		// In a real scenario, decode seqNum from buffer[:4]

		fmt.Printf("Received packet %d\n", seqNum)

		// Process packet (e.g., deliver to application)
		// ...

		// Send ACK
		ackPacket := []byte(fmt.Sprintf("ACK %d", seqNum)) // Real ACK would be a specific format
		_, err = conn.WriteToUDP(ackPacket, &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080}) // Sending back to sender
		if err != nil {
			fmt.Printf("Error sending ACK for %d: %v\n", seqNum, err)
		} else {
			fmt.Printf("Sent ACK for %d\n", seqNum)
			lastAckedSeqNum = seqNum // Update last acknowledged
		}
	}
}

To run this:

  1. Save the sender code as sender.go and receiver code as receiver.go.
  2. Open two terminal windows.
  3. In the first terminal, run: go run receiver.go
  4. In the second terminal, run: go run sender.go

You’ll see the sender sending packets and the receiver acknowledging them. If a packet were dropped (which we can’t easily simulate here without network tools), a sophisticated sender would eventually time out waiting for an ACK and retransmit.

The core problem this solves is guaranteed delivery. UDP, by itself, offers no such guarantee. Packets can be lost, duplicated, or arrive out of order. A reliability layer adds the necessary state and logic to overcome these issues.

How it works internally:

  1. Sequence Numbers: Each packet sent is assigned a unique, monotonically increasing sequence number. This allows the receiver to detect duplicates and reorder packets if they arrive out of order.
  2. Acknowledgments (ACKs): The receiver, upon successfully receiving a packet, sends back an acknowledgment to the sender. This ACK typically includes the sequence number of the packet that was received.
  3. Retransmission: The sender maintains a buffer of packets it has sent but not yet received an ACK for. If an ACK for a particular sequence number doesn’t arrive within a certain timeout period, the sender assumes the packet was lost and retransmits it.
  4. Timeout Mechanism: A crucial component is the timeout. The sender sets a timer for each unacknowledged packet. This timer’s duration needs to be carefully tuned to be long enough to account for typical network latency but short enough to detect losses quickly.
  5. Duplicate Detection: The receiver uses sequence numbers to identify and discard duplicate packets that might arrive due to retransmissions.
  6. Flow Control (Optional but common): While not strictly part of reliability, many UDP reliability layers also incorporate flow control to prevent a fast sender from overwhelming a slow receiver. This is often managed through receiver-advertised window sizes.

The exact levers you control are primarily the timeout duration and the retransmission strategy. A shorter timeout leads to quicker retransmissions but can also lead to unnecessary retransmissions if network latency spikes. A longer timeout is more forgiving of latency but slower to react to actual packet loss. Retransmission strategies can range from simple, "send it again," to more complex adaptive algorithms that adjust retransmission intervals based on observed network conditions.

The one thing most people don’t realize is that a truly robust UDP reliability layer often needs to handle out-of-order delivery as well. Simply retransmitting a packet that arrived late but was eventually received can lead to duplicate processing if the application doesn’t have its own de-duplication. The receiver must buffer packets that arrive out of order, waiting for the missing ones to be retransmitted, before delivering them to the application in the correct sequence. This adds significant complexity and state to the receiver.

The next hurdle you’ll face is handling out-of-order packet delivery gracefully.

Want structured learning?

Take the full Udp course →