DEV Community

Cover image for Building Reliable UDP
Derek mwale
Derek mwale

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The network delivers:

Packet 1

Packet 2

Packet 5
Enter fullscreen mode Exit fullscreen mode

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 |

+--------------------------------+
Enter fullscreen mode Exit fullscreen mode

Every field serves a purpose.


Rust Representation

pub struct Packet {

    pub sequence: u32,

    pub acknowledgement: u32,

    pub flags: u8,

    pub payload: Vec<u8>,

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Simple.

But incredibly important.


Why Sequence Numbers Matter

Imagine receiving:

5

6

8
Enter fullscreen mode Exit fullscreen mode

We immediately know:

Packet 7

Missing
Enter fullscreen mode Exit fullscreen mode

Without sequence numbers, missing packets are invisible.


Acknowledgements

The receiver confirms successful delivery.

Sender

↓

Packet 10

↓

Receiver

↓

ACK 10

↓

Sender
Enter fullscreen mode Exit fullscreen mode

Once acknowledged, the sender removes the packet from memory.


Packet Flow

Sender

Packet 12

──────────────►

Receiver

ACK 12

◄──────────────
Enter fullscreen mode Exit fullscreen mode

Communication becomes two-way.


Detecting Lost Packets

Suppose this happens.

Packet 13

──────────────►

Lost
Enter fullscreen mode Exit fullscreen mode

No acknowledgement arrives.

Eventually...

The sender concludes:

Packet Lost
Enter fullscreen mode Exit fullscreen mode

Retransmission Timer

Every sent packet starts a timer.

Packet Sent

↓

Timer Starts

↓

ACK Received?

↓

Yes → Done

↓

No

↓

Resend Packet
Enter fullscreen mode Exit fullscreen mode

This mechanism alone dramatically improves reliability.


Tracking Outstanding Packets

Rust.

use std::collections::HashMap;

let mut pending:

HashMap<u32, Packet>;
Enter fullscreen mode Exit fullscreen mode

Whenever an ACK arrives:

pending.remove(&ack_number);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Real protocols adapt this dynamically.


Duplicate Packets

Networks sometimes duplicate packets.

Example.

Packet 20

↓

Receiver

↓

Packet 20

↓

Receiver
Enter fullscreen mode Exit fullscreen mode

The receiver must ignore duplicates.


Duplicate Detection

Keep track of processed sequence numbers.

HashSet<u32>
Enter fullscreen mode Exit fullscreen mode

When a packet arrives:

if received.contains(

    &packet.sequence

) {

    return;

}
Enter fullscreen mode Exit fullscreen mode

Simple.

Effective.


Packet Ordering

Packets may arrive out of order.

Example.

Received

3

4

2

5
Enter fullscreen mode Exit fullscreen mode

Applications usually expect:

2

3

4

5
Enter fullscreen mode Exit fullscreen mode

We need a buffer.


Reordering Buffer

Incoming

↓

Buffer

↓

Sort

↓

Deliver
Enter fullscreen mode Exit fullscreen mode

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

+----------------------+
Enter fullscreen mode Exit fullscreen mode

As acknowledgements arrive, the window moves forward.


Sliding Window Example

Send

1

2

3

4

5

↓

ACK 1

↓

Window Advances

↓

Send 6
Enter fullscreen mode Exit fullscreen mode

The network remains busy.

Throughput improves dramatically.


Sender State

pub struct Sender {

    next_sequence: u32,

    window_size: usize,

    pending:

    HashMap<u32, Packet>,

}
Enter fullscreen mode Exit fullscreen mode

Everything revolves around outstanding packets.


Receiver State

pub struct Receiver {

    expected_sequence: u32,

}
Enter fullscreen mode Exit fullscreen mode

Whenever packets arrive:

Expected

15

↓

Receive

15

↓

Deliver

↓

Expect

16
Enter fullscreen mode Exit fullscreen mode

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

◄────────────
Enter fullscreen mode Exit fullscreen mode

Eventually every packet arrives.


Connection Setup

UDP has no connection.

We can create one.

Client

HELLO

────────────►

Server

WELCOME

◄────────────

Connected
Enter fullscreen mode Exit fullscreen mode

This allows session management.


Heartbeats

How do we know the other side still exists?

Periodic heartbeat packets.

PING

────────────►

PONG

◄────────────
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)