UDP doesn’t actually have acknowledgments, that’s TCP’s gig. What you’re likely bumping into is a custom implementation of Automatic Repeat reQuest (ARQ) over UDP, where you’re building reliability yourself on top of an unreliable transport.

Imagine you’re sending a stream of voice packets. UDP is like shouting your message across a noisy room. Most of the time, it gets there. Sometimes, it doesn’t. Sometimes, it arrives out of order. If you need to guarantee that the other side heard you, and heard you in the right order, you’ve got to build that guarantee yourself. That’s where ARQ comes in.

Let’s say you have a simple chat application. You want to send messages reliably.

Here’s a basic sender:

import socket
import time

UDP_IP = "127.0.0.1"
UDP_PORT = 5005
BUFFER_SIZE = 1024

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))

seq_num = 0
messages = ["Hello", "World", "This", "is", "reliable"]

while True:
    message = f"{seq_num}:{messages[seq_num % len(messages)]}".encode('utf-8')
    sock.sendto(message, (UDP_IP, UDP_PORT + 1)) # Send to a different port for receiver simulation

    # Wait for acknowledgment
    sock.settimeout(1.0) # 1 second timeout
    try:
        data, addr = sock.recvfrom(BUFFER_SIZE)
        ack_seq = int(data.decode('utf-8').split(':')[0])
        if ack_seq == seq_num:
            print(f"Received ACK for {seq_num}")
            seq_num += 1
            if seq_num >= len(messages):
                break
        else:
            print(f"Received out-of-order ACK {ack_seq} for {seq_num}. Resending.")
            # Resend the packet if ACK is for an older sequence number
            sock.sendto(message, (UDP_IP, UDP_PORT + 1))
    except socket.timeout:
        print(f"Timeout waiting for ACK for {seq_num}. Resending.")
        # Resend the packet if no ACK is received within the timeout
        sock.sendto(message, (UDP_IP, UDP_PORT + 1))
    time.sleep(0.1)

print("All messages sent and acknowledged.")
sock.close()

And a basic receiver:

import socket
import time

UDP_IP = "127.0.0.1"
UDP_PORT = 5006 # Receiver port
BUFFER_SIZE = 1024

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))

expected_seq_num = 0

while True:
    data, addr = sock.recvfrom(BUFFER_SIZE)
    seq_num_str, message_content = data.decode('utf-8').split(':', 1)
    seq_num = int(seq_num_str)

    if seq_num == expected_seq_num:
        print(f"Received: {message_content} (Seq: {seq_num})")
        # Send acknowledgment
        ack_message = f"{seq_num}:ACK".encode('utf-8')
        sock.sendto(ack_message, addr)
        expected_seq_num += 1
    else:
        print(f"Received out-of-order packet (Seq: {seq_num}, Expected: {expected_seq_num}). Sending ACK for last received.")
        # Send ACK for the last correctly received packet to prompt retransmission
        ack_message = f"{expected_seq_num - 1}:ACK".encode('utf-8')
        sock.sendto(ack_message, addr)

    # In a real app, you'd have a condition to break the loop
    # For this example, we'll let it run for a bit or until sender breaks
    # if expected_seq_num >= 5: # Example break condition
    #    break
    time.sleep(0.01) # Small delay to prevent busy-waiting

print("Receiver finished.")
sock.close()

When you run this, the sender sends a message with a sequence number. The receiver, if it expects that sequence number, processes the message and sends back an acknowledgment (ACK) with the same sequence number. The sender waits for this ACK. If it gets it, it moves to the next sequence number. If it times out, it assumes the packet or the ACK was lost and resends the packet. If it receives an ACK for an older sequence number, it knows its last packet might have been lost, and it resends.

The receiver also handles out-of-order packets. If it gets a packet with a sequence number it doesn’t expect, it just sends back an ACK for the last sequence number it did expect. This tells the sender, "I got up to X, but I’m still waiting for Y."

This basic ARQ, often called Stop-and-Wait ARQ, is simple but inefficient. The sender just sits there waiting. More advanced ARQ schemes, like Go-Back-N or Selective Repeat, allow the sender to send multiple packets before waiting for ACKs, significantly improving throughput.

Go-Back-N is like a slightly more impatient sender. It sends a few packets (defined by a "window size") and then waits for an ACK for the first packet it sent in that window. If that ACK is lost, it has to resend all the packets in that window, even ones the receiver might have already gotten.

Selective Repeat is the most complex and efficient. It sends a window of packets. If a packet is lost, it only resends that specific lost packet. The receiver buffers out-of-order packets and only delivers them to the application once all preceding packets have arrived. This requires more complex buffering and state management on both sides.

The core idea is that you need to:

  1. Assign sequence numbers to packets.
  2. Acknowledge received packets, referencing their sequence numbers.
  3. Implement timeouts on the sender to detect lost packets or ACKs.
  4. Resend lost packets upon timeout or receiving duplicate/out-of-order ACKs.
  5. Handle out-of-order packets on the receiver, potentially buffering them.

The most surprising true thing about implementing ARQ over UDP is how much state you have to manage on both the sender and receiver side just to achieve what TCP does natively, and how easy it is to get subtle bugs like duplicate packet delivery or lost ACKs wrong, leading to deadlocks or infinite retransmissions.

The next problem you’ll run into is defining your window size for Go-Back-N or Selective Repeat. A window that’s too small limits throughput, while a window that’s too large can flood the network or lead to excessive retransmissions if packet loss occurs.

Want structured learning?

Take the full Udp course →