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<u8>,
}
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<u32, Packet>;
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<u32>
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<u32, Packet>,
}
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.
Top comments (0)