Building Reliable UDP
How to Turn an Unreliable Protocol into One You Can Trust When most developers think about networking, they immediately think of TCP. After all, TCP powers web browsing, APIs, databases, email, and countless distributed systems. It guarantees that data arrives in order, without duplication, and without corruption. But there's another protocol that quietly powers some of the fastest systems in the world. UDP. Online multiplayer games. Live video streaming. Voice-over-IP. DNS. IoT devices. Real-time telemetry. These systems often choose UDP instead of TCP. At first, that decision seems strange. UDP doesn't guarantee delivery. It doesn't guarantee order. It doesn't prevent duplicates. It doesn't even tell you whether the receiver exists. It simply sends packets into the network and hopes for the best. That sounds terrifying. So why do engineers keep using it? Because UDP gives us something incredibly valuable: Control. Instead of accepting TCP's built-in reliability mechanisms, we can design our own—optimized for our specific application. When I first learned about reliable UDP, I assumed it was an entirely different protocol. Then I discovered something fascinating. Reliable UDP isn't a protocol. It's an engineering pattern. You're essentially rebuilding some of TCP's capabilities, but only the parts you actually need. Let's build one from scratch. Understanding UDP Imagine writing a letter and throwing it into the wind. Maybe it reaches its destination. Maybe it doesn't. That's UDP. Application │ ▼ UDP Socket │ ▼ Network │ ▼ Receiver There is no handshake. No confirmation. No guarantee. Why Would Anyone Use UDP? Because guarantees come with costs. TCP introduces: Connection setup Congestion control Flow control Retransmissions Ordered delivery Head-of-line blocking Sometimes those features are exactly what we want. Sometimes they're unnecessary. Imagine an online shooter game. If a player's position update is lost... Sending the old position two seconds later is useless. The next update will replace it anyway. UDP excels in these situations. The Problem Suppose we send five packets. Packet 1 Packet 2 Packet 3 Packet 4 Packet 5 The network delivers: Packet 1 Packet 2 Packet 5 Packets 3 and 4 disappear. UDP simply moves on. Designing Reliable UDP Instead of hoping every packet arrives, we'll build several mechanisms ourselves. Our protocol will support: Sequence numbers Acknowledgements Retransmissions Timeouts Sliding windows Duplicate detection Packet ordering Together, these transform UDP into a dependable transport layer. Packet Structure We'll begin by designing our own packet format. +--------------------------------+ | Sequence Number | +--------------------------------+ | Acknowledgement | +--------------------------------+ | Flags | +--------------------------------+ | Payload | +--------------------------------+ Every field serves a purpose. Rust Representation pub struct Packet { pub sequence: u32, pub acknowledgement: u32, pub flags: u8, pub payload: Vec, } Notice that we've added metadata on top of UDP. This metadata is entirely ours. Sequence Numbers Every outgoing packet receives an increasing identifier. Packet 1 Sequence = 1 ------------------ Packet 2 Sequence = 2 ------------------ Packet 3 Sequence = 3 Now the receiver knows exactly which packet it received. Sending Packets Our sender keeps track of the next sequence. let packet = Packet { sequence: next_sequence, acknowledgement: 0, flags: 0, payload, }; next_sequence += 1; Simple. But incredibly important. Why Sequence Numbers Matter Imagine receiving: 5 6 8 We immediately know: Packet 7 Missing Without sequence numbers, missing packets are invisible. Acknowledgements The receiver confirms successful delivery. Sender ↓ Packet 10 ↓ Receiver ↓ ACK 10 ↓ Sender Once acknowledged, the sender removes the packet from memory. Packet Flow Sender Packet 12 ──────────────► Receiver ACK 12 ◄────────────── Communication becomes two-way. Detecting Lost Packets Suppose this happens. Packet 13 ──────────────► Lost No acknowledgement arrives. Eventually... The sender concludes: Packet Lost Retransmission Timer Every sent packet starts a timer. Packet Sent ↓ Timer Starts ↓ ACK Received? ↓ Yes → Done ↓ No ↓ Resend Packet This mechanism alone dramatically improves reliability. Tracking Outstanding Packets Rust. use std::collections::HashMap; let mut pending: HashMap; Whenever an ACK arrives: pending.remove(&ack_number); Only unacknowledged packets remain. Timeouts Choosing timeout values is surprisingly important. Too short. You resend unnecessarily. Too long. Recovery becomes slow. Typical logic: Send Packet ↓ Wait 500 ms ↓ ACK? ↓ No ↓ Retransmit Real protocols adapt this dynamically. Duplicate Packets Networks sometimes duplicate packets. Example. Packet 20 ↓ Receiver ↓ Packet 20 ↓ Receiver The receiver must ignore duplicates. Duplicate Detection Keep track of processed sequence numbers. HashSet When a packet arrives: if received.contains( &packet.sequence ) { return; } Simple. Effective. Packet Ordering Packets may arrive out of order. Example. Received 3 4 2 5 Applications usually expect: 2 3 4 5 We need a buffer. Reordering Buffer Incoming ↓ Buffer ↓ Sort ↓ Deliver The application never sees disorder. Sliding Window Sending one packet at a time wastes bandwidth. Instead, we'll allow multiple outstanding packets. Window +----------------------+ 1 2 3 4 5 +----------------------+ As acknowledgements arrive, the window moves forward. Sliding Window Example Send 1 2 3 4 5 ↓ ACK 1 ↓ Window Advances ↓ Send 6 The network remains busy. Throughput improves dramatically. Sender State pub struct Sender { next_sequence: u32, window_size: usize, pending: HashMap, } Everything revolves around outstanding packets. Receiver State pub struct Receiver { expected_sequence: u32, } Whenever packets arrive: Expected 15 ↓ Receive 15 ↓ Deliver ↓ Expect 16 Simple state. Huge impact. A Complete Flow Sender Packet 1 ────────────► Receiver ACK 1 ◄──────────── Packet 2 ────────────► Lost Packet 3 ────────────► Receiver ACK 3 ◄──────────── Timeout ↓ Resend Packet 2 ────────────► Receiver ACK 2 ◄──────────── Eventually every packet arrives. Connection Setup UDP has no connection. We can create one. Client HELLO ────────────► Server WELCOME ◄──────────── Connected This allows session management. Heartbeats How do we know the other side still exists? Periodic heartbeat packets. PING ────────────► PONG ◄──────────── Miss several heartbeats. Disconnect. Congestion Awareness Sending too fast overwhelms networks. Our sender should monitor: Packet loss Round-trip time Retransmissions If loss increases: Reduce sending rate. Reliability isn't only about delivery. It's also about respecting the network. Putting Everything Together Application │ ▼ Reliable UDP Layer ┌────────────┬─────────────┐ ▼ ▼ ▼ Sequence ACK Manager Timers │ │ │ ├────────────┼─────────────┤ ▼ ▼ ▼ Retransmit Sliding Window Ordering │ ▼ UDP Socket │ ▼ Network Notice how reliability emerges from multiple cooperating components. Complete Architecture Application │ ▼ Reliable Transport API │ ▼ Packet Encoder / Decoder │ ┌───────────┼────────────┐ ▼ ▼ ▼ Sequence ACK Logic Retransmit │ │ │ ├───────────┼────────────┤ ▼ ▼ ▼ Window Ordering Heartbeats │ ▼ UDP Socket │ ▼ Internet Each layer has one responsibility. That's good software architecture. Where Real Protocols Go Further Production systems extend these ideas significantly. They include: Adaptive congestion control Forward Error Correction (FEC) Packet compression Encryption Connection migration Multipath routing Flow control Bandwidth estimation Protocols like QUIC, which powers HTTP/3, are built on UDP and implement many of these ideas to provide secure, reliable, high-performance communication. Lessons Learned Building a reliable transport protocol taught me something broader about software engineering. Reliability is rarely a single feature. It's usually the result of many small mechanisms working together. A sequence number alone isn't enough. Acknowledgements alone aren't enough. Timeouts alone aren't enough. Sliding windows alone aren't enough. But together, they create a communication layer that applications can trust. That's a recurring theme in engineering. Databases achieve reliability through transactions, logging, replication, and recovery. Distributed systems combine consensus, retries, and idempotency. Operating systems rely on schedulers, virtual memory, and interrupts. Complex behavior emerges from simple, well-designed components. Reliable UDP is another example of that principle. Final Thoughts One of the biggest misconceptions about UDP is that it's "unreliable." In reality, UDP is simply minimal. It gives developers a blank canvas. You decide what reliability means. Do you need every packet? Do you care about order? Can you tolerate duplicates? How quickly should lost packets be retransmitted? By answering those questions yourself, you can build transport protocols tailored to your application's needs instead of accepting one-size-fits-all behavior. That's why technologies like QUIC, multiplayer game networking, live streaming platforms, and many distributed systems continue to embrace UDP. Not because it's unreliable. But because it gives engineers the freedom to build exactly the reliability they need. And perhaps that's the most valuable lesson of all. Sometimes the best engineering doesn't come from adding more features. It comes from starting with something beautifully simple and carefully building only what your system truly requires.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to