Core CS · Computer Networks
Three segments to open it, four to close it
A TCP connection is not a wire. It is two numbering agreements, one per direction, held in two machines’ memory. Every plus one in the handshake, and every one of the four segments in the teardown, follows from that.
Step both endpoints through the whole lifecycle, segment by segment →01 The idea
You cannot acknowledge a byte until you know what it is called
TCP promises a reliable, ordered byte stream: what you write at one end comes out at the other end, once, in order, or the connection fails and tells you. It is built on IP, which promises none of that. IP packets can be lost, duplicated, delayed, and delivered out of order, and there is exactly one mechanism that repairs all four of those at once. Number every byte, and acknowledge by number. A receiver that says I have everything up to 1004 has simultaneously reported what arrived, what did not, and in what order.
That mechanism has a prerequisite. Before a single byte moves, both ends must agree where the numbering starts, and they must agree in both directions, because a TCP connection is full duplex: two independent byte streams running at the same time, each with its own numbering. The client’s bytes are numbered on one line and the server’s on a completely separate one. Neither line has any relationship to the other, and neither side gets to choose the other’s starting value.
It is worth being precise about what a connection actually is, because it is nothing physical. No wire is reserved, no bandwidth is booked, and no router between the two machines is told anything at all. A connection is a pair of matching records, one in each machine’s memory, called the transmission control block. Each record holds the four values that name the connection, the state it is in, and the two sequence numbers. Opening a connection means creating those two records and getting their contents to agree. Closing one means agreeing that it is now safe to delete them. That is the entire job, and every segment in this lesson exists to move one of those two records from one state to the next.
So the handshake has two things to settle at once. First, reachability and willingness: is anything actually listening at that address and port, and does it want this connection? Second, numbering: here is my starting number, tell me yours. Three segments settle both, and it is three rather than two for a reason you will be asked to give out loud: after two segments only one side has had its starting number confirmed. Agreement that only one party has is not agreement.
The close is different, and the difference comes straight from full duplex. The open can set up both directions in one exchange because neither side has sent anything yet, so there is nothing to finish. The close cannot, because the two applications stop talking at different times. A browser finishes sending its request long before the server finishes sending the page. So TCP closes one direction at a time, and each direction costs a FIN and an ACK. Two directions, two FINs, two ACKs, four segments.
02 Worked example
The three segments, with the numbers written on them
One connection, worked end to end, and it is the same connection for the rest of the lesson: the console in section 04 opens on it, the teardown table in section 03 uses it, and every number in the cheat sheet is checked against it. The client’s initial sequence number is x = 1000 and the server’s is y = 5000. Both were chosen at random; small round values are used here only so you can check the arithmetic in your head. Read left to right.
Three segments, five numbers: 1000, then 5000 and 1001, then 1001 and 5001. The first segment writes only a seq, because with no ACK flag the acknowledgement field carries nothing. Two of those five values were freely chosen, 1000 and 5000; the other three are forced. Check them the way an examiner does. The client has sent no data, so how is its next byte 1001 and not 1000? Because the SYN occupied 1000. If you write ack = 1000 on the second segment you have said I still want the SYN, and the client would retransmit it forever.
Now the highlighted node, because why three and not two is the follow-up you will get and the one that is answered badly. Suppose we stopped after two segments and let the server call itself open the moment it had sent its SYN and ACK. Two things break, and you should name both. The first is that the server would be opening a connection on the strength of a single arriving SYN, and a single SYN is easy to fake: the source address in an IP header is not verified by anything, so any machine can send a SYN claiming to be any address. The third segment is the only thing that proves the sender can actually receive at the address it claimed, because the number it has to echo, y + 1, was never on the wire in that direction. It is the closest thing TCP has to a proof of address.
The second reason is the one people forget, and it is more fundamental. After two segments the client has had its number confirmed and the server has not. The server sent y = 5000 into the network and has heard nothing back about it. If that SYN and ACK were lost, the server would sit in SYN_RCVD believing 5000 was agreed while the client was still in SYN_SENT with no connection open at all, and the very first data segment the server sent would be numbered from a value the client had never seen. The third segment is not politeness. It is the acknowledgement of y, and without it only one of the two numbering agreements exists. There is also a duplicate problem: a SYN delayed for a long time in the network can arrive after the connection it belonged to is long gone, and a two-segment scheme would open a connection on it. The third segment kills that too, because the server’s SYN and ACK goes to the real host, which has no such connection and answers with an RST rather than the expected y + 1, and the server tears the attempt down.
Two smaller points that carry marks. First, why the SYN consumes a sequence number at all when it carries no data. Because a SYN can be lost like anything else, and the only machinery TCP has for detecting a loss and retransmitting is this number has not been acknowledged yet. Give the SYN a number and it inherits the whole reliability mechanism for free, with no special case anywhere. The same argument applies word for word to the FIN. Second, why the initial sequence number is random rather than starting at zero. Two reasons. A predictable ISN lets an off-path attacker guess the numbers a connection will use and inject a segment into it without ever seeing the traffic, which was a real and widely exploited attack; RFC 6528 specifies generating the ISN from a secret hash of the four values that name the connection. And even with no attacker, a fresh random value makes it very unlikely that a straggling segment from an earlier connection between the same two ports lands inside the current connection’s window and is accepted as real data.
03 Mechanics
The states, the four segments of a close, and the fields they ride in
Three tables, in the order the questions come. First the states. A TCP endpoint is a state machine, and an interview question about this topic is almost always which state is this end in now or what moves it out of that state. The last column is the one to read carefully, because one of these rows is left only when a timer expires and one is left only when your own program acts, and confusing those two is a production incident rather than a lost mark. They are the two rows marked in amber.
| State | Whose | You arrive here by | You leave when |
|---|---|---|---|
| CLOSED | either | The start and the end. No connection record exists. | The application connects, or binds and listens. |
| LISTEN | passive | The application called listen() on a bound port. | A SYN arrives for that port. |
| SYN_SENT | active | You sent a SYN. | The SYN and ACK arrives, or you give up and time out. |
| SYN_RCVD | passive | A SYN arrived and you replied with SYN and ACK. | The third segment, the ACK, arrives. |
| ESTABLISHED | both | The handshake finished. | Either application calls close(), or a FIN arrives. |
| FIN_WAIT_1 | active closer | You sent your FIN. | Your FIN is acknowledged. |
| FIN_WAIT_2 | active closer | Your FIN was acknowledged. You are now half closed. | The peer’s FIN arrives. |
| CLOSE_WAIT | passive closer | The peer’s FIN arrived and you acknowledged it. | your own application calls close() — no timer will ever do it for you |
| CLOSING | both, rare | You and the peer sent FINs at the same instant. | Your FIN is acknowledged. Then you go to TIME_WAIT too. |
| LAST_ACK | passive closer | You sent your own FIN from CLOSE_WAIT. | The ACK of that FIN arrives. Then you are CLOSED. |
| TIME_WAIT | active closer | You acknowledged the peer’s FIN. | 2 x MSL of wall clock passes — nothing else ends it |
The row that becomes a support ticket. CLOSE_WAIT is the only state on that list that a machine can be stuck in indefinitely with nothing wrong on the network. It means the kernel has been told the peer is finished sending, has acknowledged that, and is now waiting for your program to call close() on the socket. A server that leaks file descriptors, or catches an exception on a path that skips the close, accumulates CLOSE_WAIT sockets until it runs out of descriptors and stops accepting new connections. The give-away is that the count only ever goes up. Contrast TIME_WAIT, which also accumulates in thousands on a busy machine but drains on its own and is usually not a fault at all.
Now the teardown, on the same connection as section 02. The client sent 4 bytes of data after the handshake, so it used numbers 1001 to 1004 and its next unused number is 1005. The server sent no data, so its next unused number is still 5001.
| # | Segment | Sender | On our connection | Sender is then | Receiver is then |
|---|---|---|---|---|---|
| 1 | FIN, ACK | active closer (client) | seq=1005 ack=5001 | FIN_WAIT_1 | CLOSE_WAIT |
| 2 | ACK | passive closer (server) | seq=5001 ack=1006 | CLOSE_WAIT | FIN_WAIT_2 |
| Half closed. One direction is shut and the other is not. The client will send no more data but must keep reading; the server may keep sending for as long as its application wants. Nothing here is an error and there is no timer on it. | |||||
| 3 | FIN, ACK | passive closer (server) | seq=5001 ack=1006 | LAST_ACK | TIME_WAIT |
| 4 | ACK | active closer (client) | seq=1006 ack=5002 | TIME_WAIT for 2 x MSL | CLOSED |
Read the four ack numbers, because they are the whole check. The client’s FIN sits at 1005, so the server acknowledges with 1006: the FIN consumed 1005 exactly as the SYN consumed 1000. The server’s FIN sits at 5001, the number that has been waiting unused since the handshake, so the client acknowledges with 5002. Notice that the server’s seq is 5001 in both of its segments, and that is correct rather than a typo: segment 2 is a pure ACK carrying no data, and a segment that carries nothing consumes no sequence number, so the server’s numbering did not advance between them.
Why four and not three, and the honest real-world clause. The open needed three because both directions could be set up in one exchange, with the server’s SYN riding along on the ACK of the client’s SYN. The close cannot do that in general, because the server’s ACK is due immediately (it has to confirm the client’s FIN) while the server’s FIN is not due until its application has finished writing, which could be minutes later. Two events with different timing cannot always share a segment. In practice, when the server’s application closes straight away, the stack does combine them and you see a single FIN, ACK where the table shows segments 2 and 3 — a three-segment close on the wire. Give four as the answer, then add that clause: it shows you know the reason for the four rather than the count.
Half close is a feature, not a leftover. Calling shutdown(fd, SHUT_WR) instead of close(fd) sends the FIN for your sending direction only and leaves you able to read. The classic use is a client that streams a request of unknown length: it shuts down its write side so the server sees end-of-input and can start replying, while the client stays open to read that reply. This is exactly the state between segments 2 and 3 in the table, and it can last as long as the application wants. If you are asked whether a connection where one side has sent a FIN and the other has not is broken, the answer is no, that is a legitimate half-closed connection.
TIME_WAIT, and the two jobs it is actually doing. Only the side that closed first enters it, and it stays there for 2 x MSL, twice the maximum segment lifetime — the longest a segment is allowed to survive in the network before being discarded. RFC 793 puts MSL at 2 minutes, so the textbook TIME_WAIT is 4 minutes; Linux fixes it at 60 seconds, effectively an MSL of 30 seconds. Job one: if the final ACK is lost, the peer sitting in LAST_ACK will retransmit its FIN, and somebody has to still be there to answer it. Close immediately and that retransmitted FIN meets a machine with no matching connection, which replies with an RST, and the peer’s clean shutdown turns into an error. Job two: the connection is named by a four tuple of source address, source port, destination address and destination port, and that tuple can be reused. Holding it for 2 x MSL guarantees every straggling segment from this connection has expired before a new connection can adopt the same four values, so no new connection can ever accept an old segment as its own data.
What TIME_WAIT costs, and what people wrongly do about it. The socket is gone but the four tuple is reserved, and on a machine that opens huge numbers of short outbound connections to one destination, the only part of that tuple free to vary is the source port. Ports are a 16-bit field; the IANA ranges are well known 0 to 1023, registered 1024 to 49151 and dynamic or ephemeral 49152 to 65535, and Linux’s own default range is 32768 to 60999, which is 28,232 ports. At a few thousand connections per second, held for 60 seconds each, that is exhausted in seconds. The right fixes are to reuse connections rather than reopen them, to widen the port range, or to have the other side close first so it carries the TIME_WAIT. SO_REUSEADDR lets a listener bind a port that still has connections in TIME_WAIT, and net.ipv4.tcp_tw_reuse lets outgoing connections safely reuse them; tcp_tw_recycle did something similar and broke badly behind NAT, and was removed from Linux in 4.12.
Half open, SYN floods and SYN cookies. A half-open connection is one where one side has vanished — crashed, rebooted, or been unplugged — while the other still believes the connection is ESTABLISHED. Nothing detects it until either side tries to send, at which point the survivor gets an RST, or until a keepalive probe fires; the Linux keepalive idle default is 7200 seconds, which is why a dead peer can go unnoticed for two hours. A SYN flood weaponises the gap between segments 1 and 3: the attacker sends SYNs from forged source addresses, the server allocates a record and enters SYN_RCVD for each, sends a SYN and ACK to an address that will never reply, and the third segment never comes. The half-open queue fills and real clients are refused. The defence is SYN cookies: instead of allocating anything, the server encodes the connection state into the initial sequence number it puts in the SYN and ACK, using a keyed hash it can verify later. It keeps no state at all until the third segment comes back carrying y + 1, at which point it recomputes the cookie and rebuilds the connection. The cost is that not every TCP option fits in 32 bits, so some are lost or smuggled elsewhere, which is why cookies are typically switched on only when the queue is actually under pressure.
Simultaneous open and simultaneous close, which exist and are occasionally asked. If both hosts send a SYN to each other before either receives one, neither is ever in LISTEN. Each is in SYN_SENT, receives a SYN rather than a SYN and ACK, moves to SYN_RCVD and replies with its own SYN and ACK, and both reach ESTABLISHED after four segments with no server side at all. Simultaneously closing works the same way: both send FINs, both move FIN_WAIT_1 to CLOSING rather than FIN_WAIT_2, both then acknowledge, and both ends sit in TIME_WAIT. Neither case is common on a real network, but both follow from the state table above with no new rules, which is what makes them a fair question.
Finally, the fields all of this rides in. You are expected to know the size of a TCP header and what the handshake writes into it, because the follow-up to describe the handshake is often and what does that segment actually look like.
| Field | Width | What the handshake and teardown put in it |
|---|---|---|
| Source port, destination port | 16 bits each | With the two IP addresses these form the four tuple that names the connection. Nothing else identifies it. |
| Sequence number | 32 bits | On a SYN it is the ISN. Afterwards it is the number of the first data byte in this segment; on a FIN it is the number the FIN itself occupies. |
| Acknowledgement number | 32 bits | The next byte expected, cumulative. Only meaningful when the ACK flag is set, which is why segment 1 of the handshake leaves it unused. |
| Data offset | 4 bits | Header length in 32-bit words, so 5 means the 20-byte minimum and 15 means the 60-byte maximum. That is where the options live. |
| Control bits | 6 in RFC 793 | URG, ACK, PSH, RST, SYN, FIN. The handshake uses SYN and ACK; the teardown uses FIN and ACK; a refusal or an abort uses RST. ECN later added two more bits. |
| Window | 16 bits | How much the sender is willing to receive. Both sides advertise it from their very first segment, so flow control is running before any data moves. |
| Checksum | 16 bits | Covers the header, the data, and a pseudo-header containing the two IP addresses, which is what binds a segment to the addresses it was sent between. |
| Totals. A TCP header is 20 bytes with no options and at most 60 bytes with them. A UDP header is 8 bytes, fixed, and has no sequence number and no acknowledgement number — which is precisely why UDP has no handshake to describe. | ||
05 Cheat sheet
The answers that get asked, and the wrong ones that get given
Every row is something you can be asked to state in under ten seconds. The right-hand column is the specific wrong answer that gets written down, not a general caution.
| What they ask | The answer | The trap |
|---|---|---|
| The three segments, in full | SYN seq=x · SYN,ACK seq=y ack=x+1 · ACK seq=x+1 ack=y+1 | ack=x on the second segment — the SYN already consumed x |
| Why three and not two | the third segment is the only acknowledgement of y | Saying "because TCP is reliable". Name what the third segment proves and to whom. |
| Sequence cost of a SYN or a FIN | one number each, though neither carries data | acking a FIN with the FIN’s own number — it is one higher |
| Sequence cost of a pure ACK | zero — the sender’s numbering does not advance | Advancing the server’s seq after it sends a bare ACK. |
| Why the ISN is random | blind injection by an off-path attacker, and old duplicates | Saying "for security" with nothing after it. |
| Segments to close | four: FIN, ACK, FIN, ACK | three — that is the open; the close is one FIN per direction |
| Why four | TCP is full duplex and each direction closes on its own schedule | Not naming full duplex. It is the whole reason. |
| Half closed vs half open | half closed is legal and one-directional; half open is one side gone | using them interchangeably — one is a feature, one is a fault |
| Which side gets TIME_WAIT | whichever side closed first, the active closer | Answering "the server". It depends only on who called close first. |
| How long TIME_WAIT lasts, and why | 2 x MSL · RFC 793 MSL is 2 min, so 4 min | Forgetting the two jobs: answer a retransmitted FIN, and outlive old segments. Linux fixes it at 60 s. |
| Sockets piling up in CLOSE_WAIT | the application never called close() | blaming the network — no timer clears CLOSE_WAIT |
| SYN flood, and the defence | fill the half-open queue · defend with SYN cookies | Saying "a firewall". Cookies keep no state until the third segment returns. |
| Header sizes and port ranges | TCP 20 to 60 bytes, UDP 8 · ports 0-1023, 1024-49151, 49152-65535 | quoting 20 bytes as fixed — options take it to 60 |
06 Where & why
These state names are printed by tools you will run
None of this is a teaching abstraction. The eleven state names in section 03 are literal strings that real commands print, and three of the four systems below differ from the textbook in a way worth naming out loud in an interview.
ss -tan prints one line per socket with a State column that reads LISTEN, SYN-SENT, SYN-RECV, ESTAB, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, LAST-ACK and TIME-WAIT. Note that ss abbreviates ESTABLISHED to ESTAB while the older netstat spells it out, which trips people reading two outputs side by side. A count by state, ss -tan | awk '{print $1}' | sort | uniq -c, is the first command to run on a server that has stopped accepting connections.
A SYN arriving at listen 80; does not reach nginx. It joins the kernel’s SYN queue while the connection is in SYN_RCVD, and only on the third segment does it move to the accept queue, where accept() collects it. The backlog= parameter on the listen directive sizes the second queue, capped by net.core.somaxconn, and net.ipv4.tcp_max_syn_backlog sizes the first. Overflow of the SYN queue is exactly what a SYN flood aims at, which is why net.ipv4.tcp_syncookies ships enabled on modern distributions and engages only when that queue overflows.
When a site is behind Cloudflare, the three segments complete against a Cloudflare machine near the client, not against the origin server, and a separate connection carries the request onward. That is what lets a SYN flood be absorbed: the forged SYNs never reach the origin’s SYN queue at all, and the edge answers them statelessly with cookies. It also explains a real operational consequence, that the origin sees Cloudflare’s addresses as the peer of every connection and has to read the client’s real address out of a header instead of out of the four tuple.
The filter tcp.flags.syn == 1 && tcp.flags.ack == 0 isolates every connection attempt in a capture, and adding tcp.analysis.retransmission shows the SYNs that had to be sent twice. The catch worth knowing: Wireshark displays relative sequence numbers by default, so every connection appears to start at 0 and the client’s ACK appears to carry seq=1. That is a display convenience, not the wire. Turn relative sequence numbers off in the TCP protocol preferences and the real random ISN from section 02 appears.
07 Interview questions
What they ask, and what they follow up with
This is the single most asked question in the transport layer, and almost nobody is asked it only once. The reliable pattern is that you describe the three segments, and then they ask why there are three, or what happens if one is lost, or which side waits at the end. Say the sequence numbers out loud while you draw; a description with no numbers on it reads as memorised.
Walk me through the TCP three way handshake, with the sequence numbers.
Why does it take three segments? What would go wrong with two?
Why does a SYN consume a sequence number when it carries no data?
Why is the initial sequence number random rather than zero?
Why does closing take four segments when opening takes three?
What is a half-closed connection, and is it an error?
Which side ends up in TIME_WAIT, how long does it sit there, and why does it wait at all?
So what actually breaks if a host skips TIME_WAIT and goes straight to CLOSED?
A server has thousands of sockets sitting in CLOSE_WAIT. What do you tell the team?
What is a half-open connection, and how does it differ from a half-closed one?
What is a SYN flood, and how do SYN cookies stop it?
UDP has no handshake at all. What exactly do those three segments buy you?
08 Practice problems
Six to work on paper
Draw the two sequence lines for every one of these before you write a single ack number, and mark on them what each transmission occupies. Four of the six turn on something occupying a number without carrying data, and one of them cannot be answered at all without counting two one-way trips rather than one.