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.