All notes

TCP and UDP: what I'm actually choosing between

A first attempt to understand TCP and UDP as different transport contracts rather than simply 'reliable' versus 'fast'.

about 15 minutes min read

Last month I followed some application data down through the network stack and discovered that the transport layer usually puts one of two familiar names in front of me:

TCP or UDP.

I already knew the usual shorthand:

TCP = reliable
UDP = fast

It is memorable.

It is also starting to look suspiciously incomplete.

The more useful question seems to be:

What behaviour does my application need from its transport protocol?

TCP and UDP both sit above IP and both allow applications to communicate using ports.

But they offer applications very different contracts.

Starting below them: IP

Both protocols rely on IP to move data between hosts.

The important thing is that IP itself does not provide the sort of reliable, ordered application communication that I might want.

Packets can be lost, duplicated, delayed or arrive in a different order from the one in which they were sent.

Something above IP therefore has to decide what, if anything, should be done about those possibilities.

This is where TCP and UDP make very different choices.

At a high level:

                    application
                         │
                  ┌──────┴──────┐
                  │             │
                  ▼             ▼
                 TCP           UDP
                  │             │
                  └──────┬──────┘
                         ▼
                         IP

The application chooses a transport service.

That transport service then uses IP underneath.

TCP gives me a stream

TCP provides a connection-oriented, reliable, ordered byte stream between two endpoints.

The phrase byte stream matters.

Suppose an application sends:

HELLO

and then:

WORLD

It is tempting to imagine TCP carrying two neat application messages:

[HELLO]

[WORLD]

But TCP does not preserve those application-message boundaries.

From the application’s perspective, TCP provides an ordered stream of bytes:

HELLOWORLD

The receiving application might read:

HEL

then:

LOWOR

then:

LD

depending on how it performs its reads.

So if my application has meaningful messages, my application protocol needs a way to frame them.

That might involve:

  • a fixed-size message;
  • a length prefix;
  • a delimiter;
  • some higher-level protocol structure.

TCP promises a byte stream.

It does not promise that one application write() becomes one application read() at the other end.

That is a much more useful thing to know than simply saying TCP is reliable.

TCP establishes a connection

Before ordinary application data flows, TCP establishes state between the two endpoints.

The familiar simplified picture is:

client                          server

  │                               │
  │ ----------- SYN ------------> │
  │                               │
  │ <-------- SYN + ACK ----------│
  │                               │
  │ ----------- ACK ------------> │
  │                               │
  │        connection ready       │

This is the TCP three-way handshake.

The connection lets both TCP implementations establish information required to manage the conversation, including sequence-number state.

That means TCP is stateful.

There is now transport-layer state associated with the communication between those endpoints.

UDP makes a very different choice.

TCP notices missing data

Imagine TCP needs to transport:

ABCDEFGHIJK

The data is carried through the network in TCP segments.

Conceptually, perhaps:

ABC
DEF
GHI
JK

One of those might disappear:

ABC
DEF
 X
JK

TCP uses sequence information and acknowledgements to keep track of which bytes have successfully arrived.

If required data is determined to be missing, TCP can retransmit it.

Conceptually:

sender                        receiver

 ABC  ---------------------->  ABC

 DEF  ---------------------->  DEF

 GHI  -------- X

 JK   ---------------------->  JK

      <--- missing data noticed

 GHI  ---------------------->  GHI

The real protocol is considerably more sophisticated than this sketch, but the important contract is clear:

TCP takes responsibility for reliable delivery of the byte stream.

Reliable does not mean TCP can overcome every failure indefinitely. If communication cannot be recovered, the connection can fail rather than eventually delivering the missing data.

That responsibility is not free.

It requires state, acknowledgement machinery, retransmission logic and other mechanisms.

TCP also restores order

Packets crossing an IP network do not necessarily arrive in exactly the order in which they were transmitted.

Suppose the network delivers:

1
3
2
4

For an application expecting an ordered stream, TCP cannot simply hand those bytes upwards as:

1
3
2
4

It uses its sequence information to reconstruct the correct ordering before making the data available as the ordered byte stream promised to the application.

So:

network arrival:

1  3  2  4

      │
      ▼

TCP

      │
      ▼

application receives:

1  2  3  4

This is another service the application does not have to implement itself when it chooses TCP.

It can also mean that later bytes have to wait while TCP recovers earlier missing bytes.

TCP provides flow control too

Reliability is not the only issue.

What if the sender can produce data much faster than the receiver can consume it?

TCP includes flow control, allowing a receiver to communicate how much additional data it is currently prepared to accept.

Conceptually:

fast sender
    │
    │ lots of data
    ▼

TCP receiver
    │
    └── "steady on, I only have this much space"

This helps prevent a fast sender from simply overwhelming a slower receiving endpoint.

TCP also has mechanisms concerned with congestion in the network itself, which is related but not quite the same problem.

I am filing congestion control under:

definitely coming back to this later.

UDP makes far fewer promises

UDP’s design is much smaller.

An application gives UDP a datagram.

UDP adds a small header containing fields including:

source port
destination port
length
checksum

and passes the resulting datagram to IP.

Conceptually:

┌─────────────────────────┐
│       UDP header        │
├─────────────────────────┤
│     application data    │
└─────────────────────────┘

UDP does not establish a TCP-style connection before sending the data.

It does not provide TCP-style retransmission if the datagram disappears.

It does not guarantee that datagrams arrive in order.

It does not guarantee protection against duplicate delivery.

That sounds alarming until I stop assuming every application requires those properties.

UDP preserves message boundaries

There is another important difference.

UDP is datagram-oriented.

If an application sends:

datagram A = "HELLO"

datagram B = "WORLD"

those are separate datagrams.

Assuming they arrive, the receiver does not simply receive one indistinguishable byte stream:

HELLOWORLD

The datagram boundaries remain meaningful.

Conceptually:

sender

[HELLO]
[WORLD]

     │
     ▼

UDP

     │
     ▼

receiver

[HELLO]
[WORLD]

That can be useful when the application’s natural communication model is already message-shaped.

It also means the application has different responsibilities.

UDP pushes responsibility upwards

Suppose my application requires all of these:

delivery
ordering
duplicate detection
retransmission
flow control

and I choose UDP.

Those requirements have not magically disappeared.

I have merely chosen a transport that does not provide them for me.

If my application still needs them, I now need to implement appropriate mechanisms somewhere above UDP.

Congestion control is a slightly different case. UDP does not provide it itself, but an Internet application using UDP cannot simply conclude that congestion is somebody else’s problem. Suitable congestion-control behaviour still has to exist somewhere above UDP.

This changes how I think about the choice.

UDP isn’t:

TCP with the boring safety bits removed so it goes faster.

It is a different transport service.

The application either does not require certain TCP semantics or is prepared to implement the semantics it needs itself.

“UDP is faster” is too simple

This was probably the biggest correction to my original model.

TCP does more work than UDP.

That can introduce costs.

Connection establishment, acknowledgements, retransmission, ordering, flow and congestion mechanisms all have consequences.

But:

UDP = faster

is not a sufficiently useful design rule.

An application built over UDP may need to add:

acknowledgements
retries
ordering
duplicate detection

depending on what it is trying to achieve.

And if it is sending substantial traffic across the Internet, appropriate congestion-control behaviour still has to be considered somewhere above UDP.

At that point some of the complexity has simply moved.

So instead of asking:

Which protocol is faster?

I should probably ask:

Which transport semantics does this application require?

Performance matters.

But it is not the only axis.

When freshness matters more than recovering old data

Imagine an application sends frequently updated state:

position = 101
position = 102
position = 103
position = 104

Suppose:

position = 102

gets lost.

By the time retransmission could happen, the receiver may already have:

position = 104

Depending on the application, retrieving the stale update may provide little value.

That is very different from transferring:

financial transaction
file contents
database command

where silently losing bytes would be considerably less charming.

So there are at least two very different requirements:

Application A

I need the complete byte stream in the correct order.

That sounds naturally compatible with TCP.

Application B

Give me the newest update quickly. A missing old update may be preferable to delaying newer data while recovering it.

That requirement is different.

A UDP-based protocol may be worth considering.

Neither requirement is universally better.

They are different application contracts.

UDP does have error detection

One easy mistake would be to interpret UDP as:

no checking whatsoever.

UDP includes a checksum field that can provide error detection for the UDP header and data.

There is a wrinkle for IPv4: a transmitted UDP checksum value of zero means the sender did not generate a UDP checksum. In normal Internet use, checksumming is expected, but the distinction is worth knowing.

That is different from reliability.

Detecting that received data is damaged does not imply:

UDP will retransmit it

or:

UDP guarantees another copy will arrive

So:

error detection ≠ reliable delivery

Another useful distinction for the notebook.

Ports exist in both

TCP and UDP both use port numbers.

That means:

TCP port 53

and:

UDP port 53

are not the same transport endpoint merely because the numerical port is identical.

The protocol is part of the context.

Conceptually:

IP address
+
transport protocol
+
port

helps identify the communicating endpoint.

That is why service documentation often specifies something such as:

TCP/443
UDP/53

rather than simply giving a number.

A socket gives the application access to the transport

On Linux, applications commonly interact with TCP and UDP through the socket interface.

For IPv4 Internet sockets, very roughly, a program might request:

socket(AF_INET, SOCK_STREAM, 0)

for TCP, or:

socket(AF_INET, SOCK_DGRAM, 0)

for UDP.

The SOCK_STREAM and SOCK_DGRAM types are not inherently synonymous with TCP and UDP in every socket family, so including AF_INET makes the example more precise.

Slightly confusingly, a program can also call connect() on a UDP socket. That does not create a TCP-style transport connection or handshake; it can simply associate the socket with a default peer.

That gives me another useful bridge between the networking diagrams and the process model I’ve been learning.

The application process is not manually constructing Ethernet frames.

Instead, the path looks more like:

application process
       │
       ▼
socket interface
       │
       ▼
TCP or UDP
       │
       ▼
IP
       │
       ▼
network interface

The kernel’s networking stack implements much of the machinery underneath the application’s socket.

The networking layers are therefore not merely boxes from a textbook diagram.

They are implemented pieces of software through which the process’s data actually passes.

So what am I choosing?

The choice now feels less like selecting a winner and more like deciding which semantics I want the transport to provide.

TCP gives me a connection-oriented, reliable, ordered byte stream with retransmission and flow control.

UDP gives me independent datagrams with much less transport machinery. If my application needs additional guarantees or behaviours, those responsibilities have to live somewhere above UDP.

Choosing UDP does not exempt an application from thinking about failure.

It makes the application more responsible for deciding what failure means.

A more useful comparison

So instead of:

TCP UDP
slow fast
reliable unreliable
good risky

I now prefer:

Question TCP UDP
Communication model Byte stream Datagrams
Connection-oriented Yes No TCP-style connection
Ordered delivery Provided Not provided
Reliable delivery Provided Not provided
Retransmission Provided Application responsibility if required
Message boundaries Not preserved Preserved
Flow control Provided Not provided by UDP
Application responsibility Lower for these transport concerns Higher if these behaviours are required

That seems much closer to the actual engineering decision.

One more complication: protocols can build on UDP

There is a final idea worth keeping in mind.

Choosing UDP at the transport boundary does not necessarily mean the eventual application protocol must remain primitive or unreliable.

A protocol can use UDP as a foundation and implement additional behaviour above it.

So:

application
     │
     ▼
richer protocol
     │
     ▼
UDP
     │
     ▼
IP

can still provide sophisticated communication semantics.

That tells me TCP versus UDP isn’t necessarily the final protocol-design decision.

It is the selection of a foundation with particular properties.

What gets built above that foundation is another question.

The mental model I’m keeping

The old model:

TCP
 └── reliable but slower

UDP
 └── unreliable but faster

The new one:

                 application requirements
                          │
                          ▼
               What semantics do I need?
                          │
             ┌────────────┴────────────┐
             │                         │
             ▼                         ▼
            TCP                       UDP
             │                         │
      ordered byte stream        independent datagrams
      reliable delivery          minimal transport service
      connection state           no TCP-style connection
      retransmission             no built-in retransmission
      flow control               application decides what it needs
             │                         │
             └────────────┬────────────┘
                          ▼
                          IP

The question therefore isn’t:

Which protocol is better?

It is:

Which responsibilities should belong to the transport layer, and which am I willing to leave to the application?

That feels much more like an engineering decision.

And it probably means I need to stop answering networking questions with “TCP is reliable and UDP is fast”.

References and further reading

TCP

RFC 793: Transmission Control Protocol The TCP specification in use when this note was written. It describes TCP as a connection-oriented, end-to-end reliable protocol and covers sequence numbers, acknowledgements, retransmission, ports and flow control.

RFC 9293 superseded RFC 793 in 2022; I am keeping RFC 793 here because it was the base TCP specification when this note was written.

UDP

RFC 768: User Datagram Protocol The original UDP specification. It describes UDP’s deliberately small datagram service and explicitly notes that delivery and duplicate protection are not guaranteed.

Internet host transport requirements

RFC 1122: Requirements for Internet Hosts — Communication Layers A useful comparison of the two transport services. It describes TCP as reliable and connection-oriented, while UDP provides a connectionless datagram transport service.

Linux TCP implementation

Linux tcp(7) manual page Documents Linux TCP sockets and describes the implementation as providing a reliable, stream-oriented, full-duplex connection.

Linux UDP implementation

Linux udp(7) manual page Documents Linux UDP sockets and its connectionless datagram model, including the possibility of datagrams being reordered or duplicated.