TCP vs UDP

Almost everything that crosses a network rides on one of two transport protocols: TCP (Transmission Control Protocol) and UDP (User Datagram Protocol). TCP behaves like a phone call: you connect first, and the line makes sure everything you say arrives, in order. UDP behaves like posting postcards: you just send them, quickly, and nobody promises they arrive. Understanding this one choice explains why web pages, voice calls and online games work the way they do. It is also the foundation for the rest of this series: HTTP, WebSocket and MQTT build on this.

What the Transport Layer Does

The network layer (IP) can carry a packet from one computer to another, but that is all it does. It does not say which program on the computer should receive the packet, and it makes no promise that the packet arrives at all, arrives once, or arrives in order. The transport layer adds the missing pieces:

  • Ports so that many programs on one computer can share a single IP address. Both TCP and UDP have them.
  • Optional reliability (acknowledgements, ordering, retransmission). TCP provides it, UDP does not.

Ports

A port is a 16-bit number, so it ranges from 0 to 65535. The IP address finds the computer; the port finds the program on it. Ports 0 to 1023 are the well-known ports, reserved by convention for standard services. Your own servers usually use a higher number, and a client program is typically given a temporary, high-numbered port by the operating system when it connects.

PortServiceNotes
22SSHSecure remote login, runs over TCP
53DNSName lookups. Usually UDP, with TCP used for larger answers
80HTTPPlain web traffic, runs over TCP
443HTTPSEncrypted web traffic. Typically TCP, and UDP when HTTP/3 is used
1883MQTTThe default port for unencrypted MQTT, runs over TCP

A connection is identified by both ends: the source IP address and port, and the destination IP address and port. That is why you can open ten browser tabs to the same server on port 443 at once. Each tab uses a different source port on your side, so every connection is unique.

TCP: Connect First, Then Talk

TCP is connection-oriented. Before any data moves, the two computers set up a connection with a three-way handshake. Each side picks a starting sequence number and tells the other what it expects next:

  1. SYN. The client says "I want to talk, my numbering starts at x".
  2. SYN-ACK. The server says "OK, mine starts at y, and I expect your next number to be x+1".
  3. ACK. The client says "Got it, I expect y+1 next". The connection is now open.
The life of a TCP connection A client and a server exchange SYN, SYN-ACK and ACK to open a TCP connection. The client then sends 100 bytes of data and the server acknowledges them. Finally each side closes its direction with a FIN that the other side acknowledges. Client Server Handshake 1. SYN (my numbering starts at x) 2. SYN, ACK (mine starts at y; I expect x+1) 3. ACK (I expect y+1) Data Data (100 bytes, sequence number x+1) ACK (got them; I expect x+101 next) Close FIN (I have finished sending) ACK FIN (and I have finished too) ACK
Open with three messages, exchange data that is acknowledged, and close with a FIN and an ACK in each direction. Sequence numbers count bytes, and the numbers shown here are simplified.

The handshake costs one round trip before the first byte of real data can be sent. In return, TCP gives you a set of guarantees:

  • Reliable delivery. Every byte is numbered. The receiver sends acknowledgements saying how far it has received, and the sender retransmits anything that is not acknowledged in time.
  • In-order delivery. If segments arrive out of order, TCP holds them back and hands your program the bytes in the right order.
  • Flow control. The receiver advertises a receive window, the amount of data it can still accept. A slow receiver can make a fast sender wait.
  • Congestion control. The sender watches for signs of an overloaded network, such as lost segments, and slows down. A new connection typically starts cautiously and speeds up as long as things go well.
  • Orderly closing. Each side sends a FIN when it has finished sending, and the other side acknowledges it. A RST (reset) is the abrupt alternative: it drops the connection at once. You see it, for example, when you try to connect to a port where nothing is listening, which programs report as "connection refused".

How TCP Recovers From Loss

Networks drop packets: a router is overloaded, a Wi-Fi signal is weak. TCP hides this from your program. The next figure follows three segments of data where the middle one is lost on the way.

TCP retransmitting a lost segment The sender transmits segment 1, which arrives and is acknowledged. Segment 2 is lost. Segment 3 arrives, but the receiver can only repeat its acknowledgement of 101. After a timeout the sender resends segment 2, and the receiver acknowledges everything up to byte 300. Sender Receiver Segment 1: bytes 1-100 ACK 101 (got 1-100, send 101 next) Segment 2: bytes 101-200 lost on the way Segment 3: bytes 201-300 ACK 101 again (still missing byte 101) Timer runs out (or repeated ACKs arrive): the sender resends Segment 2 again: bytes 101-200 ACK 301 (got everything up to byte 300) Your program receives bytes 1-300 in order and never sees the gap.
The receiver can only acknowledge data that arrived without a gap, so it keeps repeating ACK 101. The sender notices, resends segment 2, and the stream is whole again. Numbers are simplified, and real senders usually have many segments in flight at once.

UDP: Just Send It

UDP is connectionless. There is no handshake and no connection to close: your program hands the operating system a chunk of data and a destination address and port, and the system sends it as one packet called a datagram. That is the whole protocol. In return for the missing guarantees you get speed and simplicity:

  • No handshake. The first datagram can go out immediately, with no waiting round trip.
  • No retransmission. If a datagram is lost, nobody notices and nothing is resent, unless your own program does it.
  • No ordering. Datagrams can arrive in a different order than they were sent, and occasionally a datagram can arrive twice.
  • No flow or congestion control. UDP will send as fast as your program asks, which is a responsibility the application takes on.
  • A tiny header. Only 8 bytes, compared with at least 20 for TCP.

Message boundaries: datagrams vs a byte stream

There is one more difference that trips up beginners. UDP keeps message boundaries: if you send three datagrams, the receiver gets three separate datagrams, each exactly as it was sent (if they arrive). TCP is a byte stream. It guarantees the bytes, not how they are grouped. If you send hello and then world, the receiver may read helloworld in one go, or hel and then loworld. If your protocol has messages, you must mark where they begin and end yourself, for example with a newline or a length prefix (see Common Mistakes below).

The Headers Side by Side

Each TCP segment and UDP datagram starts with a small header placed in front of your data. The figure draws them the way specifications do, as rows that are 32 bits (4 bytes) wide. The TCP header carries all the machinery for reliability: sequence and acknowledgement numbers (orange), flags such as SYN, ACK and FIN (purple) and the receive window for flow control (green). UDP has none of it.

TCP and UDP header layouts The TCP header has source port and destination port of 16 bits each, a 32-bit sequence number, a 32-bit acknowledgement number, data offset and flags of 16 bits with a 16-bit window size, a 16-bit checksum and a 16-bit urgent pointer, for at least 20 bytes. The UDP header has a source port, destination port, length and checksum of 16 bits each, for 8 bytes. TCP header 32 bits wide Source port 16 bits Destination port 16 bits Sequence number 32 bits Acknowledgement number 32 bits Data offset + flags 16 bits: SYN, ACK, FIN Window size 16 bits Checksum 16 bits Urgent pointer 16 bits Options (optional, up to 40 bytes) Data (your payload) At least 20 bytes of header UDP header 32 bits wide Source port 16 bits Destination port 16 bits Length 16 bits Checksum 16 bits Data (your payload) Only 8 bytes of header No sequence numbers, no acknowledgements, no window, no flags: UDP does not keep track of anything between datagrams. Length counts header plus data, and the checksum detects a damaged datagram.
Every row is 32 bits (4 bytes) wide. TCP spends 12 of its 20 minimum header bytes on sequence numbers, acknowledgements, flags and window, which is exactly what UDP leaves out.

TCP vs UDP at a Glance

FeatureTCPUDP
ConnectionConnection-oriented: a three-way handshake firstConnectionless: just send
ReliabilityAcknowledgements and retransmission, so data arrives or the connection reports failureNone: datagrams may be lost or duplicated
OrderingBytes are delivered in orderNo guarantee
Flow and congestion controlYesNo
Overhead and speed20+ byte header, handshake, more state to keep. Slower to start8 byte header, no setup. Low delay
Message boundariesNone: a continuous byte streamKept: one send is one datagram
Typical usesHTTP and HTTPS, SSH, SMTP (email), FTP, MQTTDNS queries, DHCP, NTP (time), voice and video calls, live streaming, online games, CoAP

Modern protocols do not always pick a side cleanly. QUIC, which carries HTTP/3, runs on top of UDP and builds its own reliability, ordering and encryption on top of it. That lets it avoid some of TCP's limits (a lost packet stalls only one stream instead of the whole connection) while still being reliable. Whenever you need reliability on UDP, you either use a protocol like this or write that logic yourself.

Which One Should I Use?

Two questions settle most cases:

Choosing between TCP and UDP A decision flow. If every byte must arrive intact and in order, use TCP. Otherwise, if a fresh value is more useful than a perfect one, use UDP. If neither applies, start with TCP. A side note says QUIC adds its own reliability on top of UDP. Must every byte arrive, intact and in order? web pages, files, logins, email Yes Use TCP HTTP, SSH, email, MQTT No Is a fresh value more useful than a perfect one? voice, video, game state, sensor ticks Yes Use UDP DNS, calls, games, live streaming No Start with TCP Move to UDP only when you have a measured need for lower delay or tolerance of loss. Need a bit of both? QUIC (used by HTTP/3) adds its own reliability on UDP.
A rule of thumb, not a law. When in doubt, TCP is the safer default because the network stack does the hard work for you.

A live voice call shows why UDP exists. If a fraction of a second of audio is lost, it is better to skip it than to stop the conversation and wait for a retransmission that would arrive too late to be useful. A bank transfer is the opposite: a single missing or reordered byte is unacceptable, so TCP is the right tool. Small devices can use both, too. For example, an ESP32 can open TCP and UDP sockets over Wi-Fi (see ESP32 Wi-Fi).

Note: The code and commands below are meant to be run on your own computer, not on this website. They use 127.0.0.1 (localhost), so nothing leaves your machine.

Try It Yourself

A TCP echo server and client

You need Python 3. Save the server as tcp_server.py and start it first:

import socket

HOST, PORT = "127.0.0.1", 5000

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen()
    print("Waiting for a client on", HOST, PORT)
    conn, addr = server.accept()      # returns once a client has connected
    with conn:
        print("Connected by", addr)
        while True:
            data = conn.recv(1024)
            if not data:              # empty bytes means the client closed (FIN)
                break
            conn.sendall(data)        # echo the same bytes back

In a second terminal, save and run the client as tcp_client.py:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("127.0.0.1", 5000))    # the three-way handshake happens here
    s.sendall(b"hello")
    print("Echo:", s.recv(1024))

The client prints Echo: b'hello' and the server prints the address and temporary port the client was given.

A UDP sender and receiver

Start the receiver (udp_receiver.py) first, and stop it with Ctrl+C:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
    sock.bind(("127.0.0.1", 5001))
    print("Listening on UDP port 5001")
    while True:
        data, addr = sock.recvfrom(2048)
        print(addr, data.decode())

Then run the sender (udp_sender.py):

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
    for i in range(3):
        sock.sendto(f"message {i}".encode(), ("127.0.0.1", 5001))

The receiver prints three lines, one per datagram. Now stop the receiver and run the sender again: it still finishes without any error, because nothing in UDP tells the sender that nobody is listening.

Watch the connections

While the TCP server is waiting, list the open ports. On Windows use netstat -an and look for LISTENING next to 127.0.0.1:5000. On Linux use ss -tuln, where t is TCP, u is UDP, l is listening and n shows numbers instead of names.

netstat -an        # Windows
ss -tuln           # Linux

To see the handshake itself, capture traffic on the loopback interface in Wireshark and enter this display filter to show only the first packet of each new TCP connection (SYN set, ACK not set):

tcp.flags.syn == 1 && tcp.flags.ack == 0

Clear the filter, or use tcp.port == 5000, to see the whole conversation: SYN, SYN-ACK, ACK, your data, and the FIN packets at the end.

Common Mistakes

  • Assuming one send() equals one recv() on TCP. TCP has no message boundaries, so one recv() can return half a message or two messages joined together. Frame your messages: end each one with a newline, or send its length first. A length prefix looks like this:
    import struct
    
    def send_msg(sock, payload):
        sock.sendall(struct.pack("!I", len(payload)) + payload)
    
    def recv_exact(sock, n):
        buf = b""
        while len(buf) < n:
            chunk = sock.recv(n - len(buf))
            if not chunk:
                raise ConnectionError("connection closed")
            buf += chunk
        return buf
    
    def recv_msg(sock):
        (length,) = struct.unpack("!I", recv_exact(sock, 4))
        return recv_exact(sock, length)
  • Using UDP without handling loss, duplicates and reordering. If your data matters, add sequence numbers, acknowledgements and timeouts, or pick a protocol that already has them. Do not expect every datagram to arrive.
  • Sending large UDP datagrams. A typical Ethernet link carries about 1500 bytes per packet including headers. A bigger datagram is split into fragments, and if any fragment is lost the whole datagram is lost. Keeping each payload to around 1200 bytes is a commonly used safe size.
  • Forgetting that firewalls treat UDP differently. With no connection to follow, firewalls and NAT routers typically remember a UDP exchange only for a short time, and some networks block UDP altogether. A service that works over UDP at home may need a rule, or a TCP fallback, elsewhere.
  • Blaming the wrong layer. "Connection refused" usually means nothing is listening on that port (a reset came back). A timeout usually means packets are being dropped on the way, often by a firewall.
Recap: Ports let many programs share one IP address, and a connection is identified by the addresses and ports of both ends. TCP opens with a three-way handshake and then delivers an ordered, reliable byte stream using sequence numbers, acknowledgements, retransmission, flow control and congestion control, at the cost of setup time and a larger header. UDP just sends independent datagrams with an 8-byte header and no guarantees, which makes it fast and a good fit for real-time data when the application handles loss itself. Choose TCP by default, and UDP when fresh data matters more than perfect data.
Next steps: Continue to HTTP & HTTPS to see the most common protocol that runs on top of TCP, and how HTTP/3 moves to QUIC over UDP.