devto 2026-08-04 원문 보기 ↗
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.
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.
Because guarantees come with costs.
TCP introduces:
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.
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.
Instead of hoping every packet arrives, we'll build several mechanisms ourselves.
Our protocol will support:
Together, these transform UDP into a dependable transport layer.
We'll begin by designing our own packet format.
+--------------------------------+
| Sequence Number |
+--------------------------------+
| Acknowledgement |
+--------------------------------+
| Flags |
+--------------------------------+
| Payload |
+--------------------------------+
Every field serves a purpose.
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.
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.
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.
Imagine receiving:
5
6
8
We immediately know:
Packet 7
Missing
Without sequence numbers, missing packets are invisible.
The receiver confirms successful delivery.
Sender
↓
Packet 10
↓
Receiver
↓
ACK 10
↓
Sender
Once acknowledged, the sender removes the packet from memory.
Sender
Packet 12
──────────────►
Receiver
ACK 12
◄──────────────
Communication becomes two-way.
Suppose this happens.
Packet 13
──────────────►
Lost
No acknowledgement arrives.
Eventually...
The sender concludes:
Packet Lost
Every sent packet starts a timer.
Packet Sent
↓
Timer Starts
↓
ACK Received?
↓
Yes → Done
↓
No
↓
Resend Packet
This mechanism alone dramatically improves reliability.
Rust.
use std::collections::HashMap;
let mut pending:
HashMap<u32, Packet>;
Whenever an ACK arrives:
pending.remove(&ack_number);
Only unacknowledged packets remain.
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.
Networks sometimes duplicate packets.
Example.
Packet 20
↓
Receiver
↓
Packet 20
↓
Receiver
The receiver must ignore duplicates.
Keep track of processed sequence numbers.
HashSet<u32>
When a packet arrives:
if received.contains(
&packet.sequence
) {
return;
}
Simple.
Effective.
Packets may arrive out of order.
Example.
Received
3
4
2
5
Applications usually expect:
2
3
4
5
We need a buffer.
Incoming
↓
Buffer
↓
Sort
↓
Deliver
The application never sees disorder.
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.
Send
1
2
3
4
5
↓
ACK 1
↓
Window Advances
↓
Send 6
The network remains busy.
Throughput improves dramatically.
pub struct Sender {
next_sequence: u32,
window_size: usize,
pending:
HashMap<u32, Packet>,
}
Everything revolves around outstanding packets.
pub struct Receiver {
expected_sequence: u32,
}
Whenever packets arrive:
Expected
15
↓
Receive
15
↓
Deliver
↓
Expect
16
Simple state.
Huge impact.
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.
UDP has no connection.
We can create one.
Client
HELLO
────────────►
Server
WELCOME
◄────────────
Connected
This allows session management.
How do we know the other side still exists?
Periodic heartbeat packets.
PING
────────────►
PONG
◄────────────
Miss several heartbeats.
Disconnect.
Sending too fast overwhelms networks.
Our sender should monitor:
If loss increases:
Reduce sending rate.
Reliability isn't only about delivery.
It's also about respecting the network.
Application
│
▼
Reliable UDP Layer
┌────────────┬─────────────┐
▼ ▼ ▼
Sequence ACK Manager Timers
│ │ │
├────────────┼─────────────┤
▼ ▼ ▼
Retransmit Sliding Window Ordering
│
▼
UDP Socket
│
▼
Network
Notice how reliability emerges from multiple cooperating components.
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.
Production systems extend these ideas significantly.
They include:
Protocols like QUIC, which powers HTTP/3, are built on UDP and implement many of these ideas to provide secure, reliable, high-performance communication.
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.
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.