How TCP works
How the Transmission Control Protocol turns unreliable packets into a reliable stream of bytes, from ports and the three-way handshake to acknowledgments, retransmission, flow control and congestion control.
The Internet Protocol delivers packets on a best-effort basis: a packet may be lost, delayed, duplicated or overtaken by a later one, and IP does not notice. Yet when you load a web page or download a file, every byte arrives, in order and exactly once. The protocol that makes this possible is the Transmission Control Protocol (TCP).
TCP was specified in RFC 793 in 1981. Four decades of fixes and extensions were collected into a new specification, RFC 9293, in 2022, but the core design has not changed. This article explains how TCP sets up a connection, numbers and acknowledges data, recovers from loss, avoids overwhelming the receiver and the network, and closes the connection again.
What TCP provides
TCP gives two programs a connection: a private, two-way channel between them. Through it:
- Bytes arrive reliably. Lost data is detected and sent again.
- Bytes arrive in order, and duplicates are discarded.
- Data flows both ways at once (full duplex).
- The sender never overwhelms the receiver (flow control).
- The sender adapts to the network, slowing down when it is congested (congestion control).
TCP carries a stream of bytes, not messages. If a program writes 100 bytes and then another 100, the other side may receive them as one block of 200 or as several smaller pieces. Protocols built on TCP, such as HTTP, mark where their messages begin and end themselves.
What TCP does not provide is just as important: it makes no promise about when data arrives. Reliability is bought with waiting, because a lost piece must be sent again before anything after it can be delivered.
Ports and connections
An IP address identifies a device. To tell apart the many programs on that device, TCP adds 16-bit port numbers, from 0 to 65,535. A server listens on a well-known port, such as 443 for HTTPS or 22 for SSH. The client’s operating system picks a temporary ephemeral port for its side.
A connection is identified by four values together: the source address, the source port, the destination address and the destination port. Two connections from the same browser to the same server differ in their ephemeral ports, so each is a connection of its own.
Port numbers are managed by IANA in three ranges (RFC 6335):
| Range | Name | Used for |
|---|---|---|
| 0–1023 | System ports | Well-known services such as HTTP (80), HTTPS (443), SSH (22) |
| 1024–49151 | User ports | Registered services, such as many databases |
| 49152–65535 | Dynamic ports | Ephemeral ports; never registered |
Operating systems choose their own ranges for ephemeral ports, which do not always match IANA’s dynamic range.
The TCP segment
TCP sends data in segments, each carried in one IP packet. Every segment starts with a header of at least 20 bytes.
- Source port and destination port identify the programs at both ends.
- The sequence number gives the position of the segment’s first byte in the stream.
- The acknowledgment number gives the next byte the sender of the segment expects to receive, which confirms everything before it.
- Data offset is the header length in 32-bit words, so the header is 20 to 60 bytes long.
- The control bits, often called flags, mark special segments.
SYNopens a connection,ACKsays the acknowledgment number is valid (it is set on almost every segment),FINcloses one direction, andRSTaborts the connection.PSHasks the receiver to pass data to the program without waiting;URGmarks urgent data, a feature that is rarely used;ECEandCWRcarry explicit congestion notifications (RFC 3168). - The window tells the other side how many more bytes the sender of the segment can accept.
- The checksum covers the header, the data and a summary of the IP header, so a damaged segment is discarded.
- Options add features, mostly negotiated when the connection opens: the maximum segment size, window scaling, selective acknowledgments and timestamps.
Opening a connection: the three-way handshake
Before any data flows, both sides must agree to talk and learn where each other’s numbering starts. TCP does this with three segments, the three-way handshake.
- SYN. The client sends a segment with the SYN flag and its initial sequence number (ISN), called x here.
- SYN-ACK. The server answers with its own SYN and ISN, y, and acknowledges the client’s SYN with the acknowledgment number x + 1.
- ACK. The client acknowledges the server’s SYN with y + 1. The connection is open.
A SYN and a FIN each count as one byte in the sequence space, which is why the acknowledgments read x + 1 and y + 1 although no data has been sent. Three segments are the minimum because each side must send its own starting number and hear that the other side received it. The third segment may already carry data.
The initial sequence numbers are not zero. Each side chooses a number that is hard to predict (RFC 9293, which absorbed the earlier RFC 6528), so that an attacker who is not on the path cannot inject forged segments into the connection, and so that segments from an old connection between the same ports cannot be mistaken for new ones.
The SYN segments also carry options that both sides must agree on from the start. The most important is the maximum segment size (MSS): the largest amount of data each side is willing to receive in one segment. On an Ethernet network with packets of 1,500 bytes, it is typically 1,460 bytes for IPv4 (1,500 minus 20 bytes of IP header and 20 of TCP header). If a side announces no MSS, the other must assume 536 bytes for IPv4 or 1,220 bytes for IPv6.
A server must remember every half-open connection until the handshake completes. Attackers abuse this with SYN floods, sending masses of SYN segments that are never completed. A common defence is SYN cookies (RFC 4987): the server encodes the connection details in its own initial sequence number and keeps no state at all until the final ACK comes back.
Sequence numbers and acknowledgments
TCP numbers every byte, not every segment. An example with round numbers: a client whose initial sequence number was 4,000 sends 2,400 bytes in two segments of 1,200 bytes each.
| Segment | Sequence number | Bytes | The receiver answers |
|---|---|---|---|
| First | 4,001 | 4,001–5,200 | ACK 5,201 |
| Second | 5,201 | 5,201–6,400 | ACK 6,401 |
Each acknowledgment names the next byte the receiver expects. Acknowledgments are cumulative: ACK 6,401 confirms every byte before 6,401, so if the first acknowledgment is lost, the second one covers it.
Now suppose the first segment is lost and only the second arrives. The receiver cannot deliver bytes 5,201 to 6,400 to the program yet, because bytes 4,001 to 5,200 come first. It keeps the second segment and answers again with ACK 5,201: a duplicate acknowledgment that says “I am still waiting for byte 5,201”. With the selective acknowledgment (SACK) option (RFC 2018), it can add “but I already have 5,201 to 6,400”, so the sender resends only what is missing.
Recovering from loss
TCP detects loss in two ways.
Timeouts. For every segment it sends, TCP expects an acknowledgment within a retransmission timeout (RTO). If none arrives, it sends the segment again. The timeout adapts to the measured round-trip time (RFC 6298): TCP keeps a smoothed average of the round-trip time and of its variation, and sets the RTO to the average plus four times the variation, with a minimum of one second. Each time a timer expires, the timeout doubles, so a sender facing a dead connection backs off quickly instead of flooding the network.
Fast retransmit. Waiting for a timeout is slow, and duplicate acknowledgments often reveal a loss much sooner. When a sender receives three duplicate acknowledgments in a row, it assumes the segment they ask for was lost and resends it at once (RFC 5681). Modern implementations add time-based loss detection, such as RACK-TLP (RFC 8985), which also catches losses near the end of a transfer, where there may be too little data to produce three duplicates.
Flow control: the receive window
A fast sender could easily overwhelm a slow receiver. To prevent this, every segment carries a window: the number of bytes, counted from the acknowledgment number, that its sender is currently able to accept. The other side may have at most that many unacknowledged bytes in flight.
As the receiving program reads data, space frees up, and the receiver announces a larger window. If the program stops reading, the window shrinks to zero and the sender pauses. It then sends small window probes from time to time, so that it notices when the window reopens.
The window field has only 16 bits, so it cannot exceed 65,535 bytes. That limits the speed of a single connection: at most one window of data can be in flight per round trip. To keep a link busy, the window must be at least the bandwidth-delay product. For a 100 Mbit/s path with a round-trip time of 50 milliseconds, that is 100,000,000 × 0.05 = 5,000,000 bits, or 625,000 bytes, almost ten times the limit. The window scale option (RFC 7323), agreed in the SYN segments, multiplies the window by a power of two of up to 2¹⁴, allowing windows of up to about one gigabyte.
Congestion control
Flow control protects the receiver. The network needs protection too: if every sender transmitted as fast as its receiver allowed, routers would fill their queues and drop packets, senders would retransmit, and the extra traffic would make things worse. This failure, called congestion collapse, was first observed in the mid-1980s, as the early internet grew (RFC 2914). Beginning in 1986, Van Jacobson developed the congestion avoidance mechanisms, published in 1988, that are the basis of what TCP still does today.
The sender keeps a second window of its own, the congestion window (cwnd), and never has more data in flight than the smaller of the congestion window and the receiver’s window. It adjusts the congestion window as it learns about the network (RFC 5681):
- Slow start. A new connection starts with a small window and grows it by one segment for every acknowledged segment, which roughly doubles the window every round trip. The name refers to starting small; the growth itself is fast.
- Congestion avoidance. Once the window reaches the slow start threshold (ssthresh), it grows more carefully, by about one segment per round trip.
- Loss signals congestion. On three duplicate acknowledgments, the sender halves its window and continues from there (fast recovery). On a timeout, which suggests heavier congestion, it sets the threshold to half the data in flight and starts over from one segment.
This pattern of adding slowly and cutting sharply is called additive increase, multiplicative decrease (AIMD). It lets many connections share a link fairly and keeps the network stable.
The diagram shows the classic algorithm with round numbers. Real implementations have refined it in several ways:
- A larger start. The initial window was raised to ten segments (RFC 6928), so that small transfers such as most web pages finish in fewer round trips.
- CUBIC (RFC 9438) grows the window along a cubic curve that depends on the time since the last loss rather than on the round-trip time. It uses fast, long-distance links better, and it is the default in the Linux, Windows and Apple network stacks.
- BBR, developed at Google, does not treat loss as the main signal. It measures the path’s delivery rate and round-trip time, builds a model of the path from them, and sends at the rate the path can carry. Its specification is still an Internet-Draft at the IETF.
- ECN (RFC 3168) lets routers mark packets instead of dropping them when their queues start to
fill. The receiver echoes the mark with the
ECEflag, and the sender slows down as if a packet had been lost, without losing one.
Small segments: Nagle and delayed acknowledgments
Sending many tiny segments is wasteful: a single typed character in a 40-byte header is mostly overhead. Nagle’s algorithm (RFC 896) therefore holds back small segments while earlier data is still unacknowledged, and sends the collected bytes together when the acknowledgment arrives.
Receivers save segments too. Instead of acknowledging every segment at once, a receiver may wait briefly, hoping to combine the acknowledgment with data of its own. Such a delayed acknowledgment must be sent within half a second, and at least every second full-sized segment should be acknowledged (RFC 1122).
Each technique is sensible on its own, but together they can stall an exchange: the sender waits
for an acknowledgment before sending its last small piece, while the receiver waits for more data
before acknowledging. Latency-sensitive programs, such as interactive terminals or games, often
switch Nagle’s algorithm off with the socket option TCP_NODELAY.
Closing a connection
Each direction of a connection is closed separately. A side that has nothing more to send sends a segment with the FIN flag; the other side acknowledges it and may keep sending until it is done too. The usual close therefore takes four segments, although the middle two are often combined.
The side that closes first ends in the TIME-WAIT state and stays there for twice the maximum segment lifetime (MSL), which the specification sets at two minutes. The wait serves two purposes. If its final acknowledgment is lost, the other side will resend its FIN, and the connection must still exist to answer it. And any delayed segments from the old connection must die out before the same four values (addresses and ports) can be used for a new one. The specification calls the two minutes an engineering choice, and many systems use a shorter wait in practice.
A connection can also end abruptly. A segment with the RST flag aborts it immediately, without waiting for acknowledgments. A host sends RST, for example, when a segment arrives for a port where no program is listening, or when a program closes a connection that still has unread data.
In total, a TCP connection moves through eleven states: CLOSED, LISTEN, SYN-SENT, SYN-RECEIVED,
ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK and TIME-WAIT. The tools
ss on Linux and netstat on most systems show the state of every connection on a machine.
TCP and UDP
TCP’s sibling in the transport layer is the User Datagram Protocol (UDP), specified in RFC 768 in 1980. Its header has only four fields: the two ports, a length and a checksum. UDP adds ports to IP and nothing else.
| Aspect | TCP | UDP |
|---|---|---|
| Connection | Set up with a handshake | None: each datagram stands alone |
| Delivery | Reliable, ordered, without duplicates | Best effort, like IP |
| Unit of data | A stream of bytes | Separate datagrams, boundaries kept |
| Flow and congestion control | Built in | None; the application must take care |
| Header | 20 to 60 bytes | 8 bytes |
| Typical uses | Web, email, file transfer, remote login | DNS, voice and video calls, games, QUIC |
UDP suits applications that prefer fresh data to complete data, such as a video call, where a late frame is worthless, and applications that build their own reliability on top.
Where TCP reaches its limits
TCP’s reliability has a price. Because bytes are delivered strictly in order, one lost segment holds back everything behind it, even data that has already arrived. When several independent transfers share one connection, as in HTTP/2, a single loss stalls all of them. This is called head-of-line blocking.
Setting up a connection also costs time: one round trip for the TCP handshake, plus at least one more for TLS encryption, before the first request can be sent. Extensions such as TCP Fast Open (RFC 7413), which allows data in the SYN segment, help but are hard to deploy, because firewalls and other devices on the path often interfere with unfamiliar TCP features.
These limits led to QUIC (RFC 9000), a newer transport protocol that runs over UDP. It combines the transport and encryption handshakes, delivers independent streams independently, and is the foundation of HTTP/3. TCP remains the workhorse of the internet, and its ideas live on in QUIC.
Key points
- TCP turns IP’s unreliable packets into a reliable, ordered byte stream between two programs, identified by addresses and ports.
- A three-way handshake (SYN, SYN-ACK, ACK) opens a connection and synchronizes the sequence numbers.
- Every byte has a sequence number. Cumulative acknowledgments, timeouts, duplicate acknowledgments and SACK detect and repair loss.
- The receive window protects the receiver; the congestion window protects the network, growing gradually and shrinking sharply on loss.
- Connections close one direction at a time with FIN, and the closing side waits in TIME-WAIT. QUIC carries these ideas forward over UDP.
Sources
- Transmission Control Protocol (TCP) (RFC 9293)W. Eddy, RFC Editor, 2022Accessed
- Transmission Control Protocol (RFC 793)J. Postel, RFC Editor, 1981Accessed
- User Datagram Protocol (RFC 768)J. Postel, RFC Editor, 1980Accessed
- Internet Assigned Numbers Authority (IANA) Procedures for the Management of the Service Name and Transport Protocol Port Number Registry (RFC 6335)M. Cotton, L. Eggert, J. Touch, M. Westerlund, S. Cheshire, RFC Editor, 2011Accessed
- TCP SYN Flooding Attacks and Common Mitigations (RFC 4987)W. Eddy, RFC Editor, 2007Accessed
- Computing TCP's Retransmission Timer (RFC 6298)V. Paxson, M. Allman, J. Chu, M. Sargent, RFC Editor, 2011Accessed
- TCP Selective Acknowledgment Options (RFC 2018)M. Mathis, J. Mahdavi, S. Floyd, A. Romanow, RFC Editor, 1996Accessed
- The RACK-TLP Loss Detection Algorithm for TCP (RFC 8985)Y. Cheng, N. Cardwell, N. Dukkipati, P. Jha, RFC Editor, 2021Accessed
- TCP Extensions for High Performance (RFC 7323)D. Borman, B. Braden, V. Jacobson, R. Scheffenegger, RFC Editor, 2014Accessed
- Congestion Control Principles (RFC 2914)S. Floyd, RFC Editor, 2000Accessed
- Congestion Avoidance and ControlV. Jacobson, ACM SIGCOMM Computer Communication Review, 1988
- TCP Congestion Control (RFC 5681)M. Allman, V. Paxson, E. Blanton, RFC Editor, 2009Accessed
- Increasing TCP's Initial Window (RFC 6928)J. Chu, N. Dukkipati, Y. Cheng, M. Mathis, RFC Editor, 2013Accessed
- CUBIC for Fast and Long-Distance Networks (RFC 9438)L. Xu, S. Ha, I. Rhee, V. Goel, L. Eggert, RFC Editor, 2023Accessed
- BBR Congestion Control (draft-ietf-ccwg-bbr-06)N. Cardwell, I. Swett, J. Beshay, IETF, 2026Accessed
- The Addition of Explicit Congestion Notification (ECN) to IP (RFC 3168)K. Ramakrishnan, S. Floyd, D. Black, RFC Editor, 2001Accessed
- Congestion Control in IP/TCP Internetworks (RFC 896)J. Nagle, RFC Editor, 1984Accessed
- Requirements for Internet Hosts — Communication Layers (RFC 1122)R. Braden, RFC Editor, 1989Accessed
- TCP Fast Open (RFC 7413)Y. Cheng, J. Chu, S. Radhakrishnan, A. Jain, RFC Editor, 2014Accessed
- QUIC: A UDP-Based Multiplexed and Secure Transport (RFC 9000)J. Iyengar, M. Thomson, RFC Editor, 2021Accessed