What Is TCP? How the Transmission Control Protocol Works

TCP explained: the three-way handshake, ports, connection states and TCP vs UDP, with real tcpdump and ss output and commands to check your server.

Short answer: TCP (Transmission Control Protocol) is the part of the internet’s protocol stack that gives two programs a reliable, in-order stream of bytes. It numbers the data it sends, spots lost or damaged pieces, sends them again and slows down when the network is busy. Web browsing over HTTP/1.1 and HTTP/2, SSH, email and most database connections run on TCP. The current standard is RFC 9293, which replaced the original RFC 793.

tcpdump output showing the SYN, SYN-ACK and ACK packets of a TCP three-way handshake
A real TCP three-way handshake captured with tcpdump: [S] SYN, [S.] SYN-ACK, [.] ACK.

What TCP does for an application

IP, the Internet Protocol, moves packets from one address to another, but it makes no promises: packets can arrive late, out of order, twice or not at all. TCP sits on top of IP and hides that from the application. According to RFC 9293:

  • Reliable, in-order byte stream. What one side writes, the other side reads in the same order.
  • Segments inside IP packets. The stream is cut into TCP segments, and each segment travels as an IP datagram.
  • Loss and error detection. Sequence numbers show what is missing, a checksum on every segment shows what is damaged, and missing data is sent again.
  • Connections. Both sides set up a connection before sending data and close it afterwards. Data can flow both ways.
  • Ports. Port numbers identify the service (443 for HTTPS, 22 for SSH) and let one server handle thousands of connections at once.
  • Congestion control. Every TCP implementation must slow down when the network is congested, so one busy connection does not swamp everyone else.

The three-way handshake

Before any data moves, the client and server exchange three segments. We captured this with tcpdump 4.99.5 in a Debian 13 container while curl requested a page from a small web server on port 8080 (output trimmed to the important parts):

127.0.0.1.41380 > 127.0.0.1.8080: Flags [S],  seq 4210159877, win 65495, options [mss 65495,...]
127.0.0.1.8080 > 127.0.0.1.41380: Flags [S.], seq 4120032608, ack 4210159878, win 65483, ...
127.0.0.1.41380 > 127.0.0.1.8080: Flags [.],  ack 4120032609, win 512, ...
127.0.0.1.41380 > 127.0.0.1.8080: Flags [P.], seq 4210159878:4210159956, ... length 78: HTTP: GET / HTTP/1.1
  1. SYN ([S]): the client asks to open a connection and sends its starting sequence number.
  2. SYN-ACK ([S.]): the server agrees, sends its own starting number and acknowledges the client’s number plus one.
  3. ACK ([.]): the client acknowledges the server’s number. The connection is now ESTABLISHED and the HTTP request follows ([P.], 78 bytes).

In tcpdump’s notation, S is SYN, F is FIN, P is PUSH, R is RST and a dot means ACK. The win value is the receive window: how many bytes the sender of that segment can accept right now. That is how TCP stops a fast sender from overrunning a slow receiver.

Closing a connection and TIME-WAIT

Each side sends a FIN when it has finished sending, and the other side acknowledges it. The side that closes first then stays in the TIME-WAIT state for twice the Maximum Segment Lifetime, so that stray segments from the old connection cannot be mistaken for a new one. RFC 9293 takes the MSL to be 2 minutes as an engineering choice.

After our test request, ss -tan showed the finished connection in that state (other lines removed):

State     Recv-Q Send-Q Local Address:Port   Peer Address:Port
LISTEN    0      5          127.0.0.1:8080        0.0.0.0:*
TIME-WAIT 0      0          127.0.0.1:41380     127.0.0.1:8080

On a busy web server you will often see many TIME-WAIT entries. On their own they are part of normal TCP behaviour, not a sign of an attack.

Ports: how one server runs many services

A TCP connection is identified by four values: client IP, client port, server IP and server port. RFC 6335 splits the 65,536 port numbers into three ranges: System Ports 0 to 1023, User Ports 1024 to 49151 (both assigned by IANA) and Dynamic Ports 49152 to 65535, which are never assigned. Operating systems pick the client side from a local range; on our Debian 13 test container it was 32768 to 60999 (/proc/sys/net/ipv4/ip_local_port_range).

Common TCP ports from the IANA port registry:

PortService
22SSH (and SFTP)
25SMTP, server-to-server email
53DNS (TCP and UDP)
80HTTP
443HTTPS
587Email submission from mail clients
993IMAP over TLS
3306MySQL and MariaDB
3389Remote Desktop (RDP)
5432PostgreSQL

For the email and TLS ports in more detail, see HTTPS and SSL port numbers.

TCP vs UDP

UDP, the User Datagram Protocol (RFC 768), is TCP’s lightweight sibling. It sends single messages with “a minimum of protocol mechanism”, and delivery and duplicate protection are not guaranteed. Applications that need ordered, reliable delivery are pointed to TCP by the UDP standard itself.

TCPUDP
ConnectionHandshake first, then dataNo handshake, just send
DeliveryLost data is resentNot guaranteed
OrderAlways in orderMay arrive out of order
Header20 bytes plus options8 bytes
Speed controlFlow and congestion control built inUp to the application
Typical usesWebsites, SSH, email, databases, file transferDNS queries, QUIC (HTTP/3), game traffic such as Minecraft Bedrock gameplay

Two things blur the line. DNS uses both: RFC 7766 makes TCP support a requirement for DNS software (see also when DNS uses TCP or UDP). And HTTP/3 runs over QUIC, a newer connection-oriented transport that carries its packets in UDP datagrams (RFC 9000). That is why IANA lists port 443 for both TCP and UDP.

For game servers, the protocol decides which firewall rule you need. A Minecraft Java Edition 26.3 server, for example, listened on TCP port 25565 in our test. Open the port for the right protocol, or players will not get in.

Checking TCP on your own server

These commands were run on Debian 13 and work the same on Ubuntu and AlmaLinux.

Which services are listening?

sudo ss -tlnp
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      5          127.0.0.1:8080      0.0.0.0:*    users:(("python3",pid=3691,fd=3))

-t means TCP, -l listening sockets, -n numbers instead of names and -p the process. A service bound to 127.0.0.1 only accepts connections from the server itself. More options are in how to find which process is using a port.

Can I reach a port from outside?

nc -vz 127.0.0.1 8080
nc -vz 127.0.0.1 8081
Connection to 127.0.0.1 8080 port [tcp/http-alt] succeeded!
nc: connect to 127.0.0.1 port 8081 (tcp) failed: Connection refused

“Connection refused” means the server answered with a reset: RFC 9293 says a SYN that does not match a connection is rejected this way, so nothing is listening on that port. A connection that hangs until it times out usually means a firewall is dropping the packets instead. That difference tells you where to look. The nc guide has more examples.

UDP has no handshake, so this kind of check is far less useful there. In our test, nc -vzu against a UDP port with nothing listening printed nothing at all, neither success nor failure.

Open a port in the firewall

sudo ufw allow 25565/tcp
sudo ufw allow 51820/udp     # a UDP service, for example WireGuard

With ufw 0.36.2 each command answered Rules updated and Rules updated (v6). Name the protocol every time; the TCP rule does nothing for UDP traffic on the same number.

Count connections by state

ss -s
ss -tan state established
ss -tan state time-wait

ss -s prints a summary line such as TCP: 126 (estab 0, closed 126, orphaned 0, timewait 2). A sudden jump in established connections to your web port is worth a closer look. To watch ports change live, see how to watch TCP and UDP ports in real time.

TCP connection states at a glance

StateMeaning (from RFC 9293)
LISTENWaiting for a connection request
SYN-SENTSent a SYN, waiting for the reply
SYN-RECEIVEDReceived and sent a SYN, waiting for the final ACK
ESTABLISHEDOpen connection, data flowing
FIN-WAIT-1 / FIN-WAIT-2This side closed, waiting for the other side
CLOSE-WAITThe other side closed, waiting for the local program to close
LAST-ACK / CLOSINGWaiting for the final acknowledgement
TIME-WAITClosed, waiting for stray segments to die out

Many connections stuck in CLOSE-WAIT usually point at the application: the other side has hung up, but the program on your server has not closed its end.

Knowing TCP makes most “cannot connect” problems quicker to solve: check that the service listens, that the firewall allows the right protocol, and whether you get a refusal or a timeout. If you would rather have someone else do the digging, our Linux server management team can check ports, firewalls and services for you.

Ria Febin
Ria Febin