
3-Way Handshake: Start of Relation
TCP is polite. Before creating a connection, it greets 3 times. SYN, SYN-ACK, ACK. Why?

TCP is polite. Before creating a connection, it greets 3 times. SYN, SYN-ACK, ACK. Why?
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Establishing TCP connection is expensive. Reuse it for multiple requests.

When I first learned about TCP, the term "Connection-oriented protocol" didn't make much sense to me. Then one day, while making a phone call, it clicked. Oh, this is exactly like making a phone call!
Think about when we make a phone call:
Data transmission starts only after this 3-step greeting. Without this process? I'd be talking to myself, not knowing if the other person is even listening or if the line is dead. When I was debugging network errors in my service, understanding this concept made it so much easier to pinpoint where things went wrong.
At first, I thought "Oh, it's just 3 greetings." But I learned there's much more happening under the hood. It's actually exchanging Sequence Numbers to prepare for reliable communication.
The client tells the server "Let's connect. My Initial Sequence Number (ISN) is 100." The packet flags are SYN=1, ACK=0, and the client enters SYN_SENT state.
The important part here is that the client sends a randomly generated ISN of 100. It's saying "I'll start numbering my data from this number." I initially wondered "Why not start from 0?" Later I learned it's for security reasons (preventing sequence number prediction attacks) and to distinguish old packets from new ones.
The server responds to the client: "Got it (ACK 101). I hear you. I'm also connecting. My ISN is 2000 (SYN)." Packet flags are SYN=1, ACK=1, and the server enters SYN_RCVD state.
What's interesting about this step is that it does two things at once:
Since it's bidirectional communication, the server is also saying "I have things to say too." Understanding this made it clear why it's called "3-Way."
The client sends the final confirmation to the server: "Got your 2000 (ACK 2001). Connection established." Packet flags are SYN=0, ACK=1, and the client enters ESTABLISHED state. When the server receives this, it also becomes ESTABLISHED.
From this point on, a logical connection tunnel (Socket) is considered created. Now we can exchange data.
This was my biggest question when I first learned this. "Why can't we just send SYN, get SYN-ACK, and start?" This is actually a commonly asked question too.
Networks are unreliable. Packets can arrive late or out of order. Imagine this scenario:
SYN_A, but it gets delayed due to network congestionSYN_B), completes communication, and closesSYN_A finally arrives at the serverIf it was 2-Way Handshake? The server sees SYN_A and immediately thinks "Oh, new connection!" and establishes the connection, allocating memory. But the client is already done. The server is left talking to empty air, wasting resources.
With 3-Way Handshake:
SYN_A and sends SYN-ACKIn other words, the 3rd ACK is the final confirmation stamp saying "this connection request is valid right now." Understanding this made me appreciate why TCP was designed this way.
Closing a connection takes 4 steps, not 3. Why? Because there might still be data in transit.
FIN_WAIT_1CLOSE_WAITLAST_ACKTIME_WAIT → CloseThe client doesn't die immediately after sending the final ACK. It stays alive in TIME_WAIT state for about 2 minutes. At first I thought "Why not close immediately?" but there's a reason.
If the final ACK gets lost? The server thinks "Didn't they get my FIN?" and retransmits FIN. If the client is already dead? The server can never close properly and gets an error. So the client waits a bit, thinking "Just in case they didn't hear my last message."
I was shocked when I ran netstat -an | grep TIME_WAIT on my service and saw thousands of them. Turns out it was a signal that the client was connecting and disconnecting too frequently. To solve this, I needed to use a Connection Pool. Instead of doing 3-Way Handshake every time (CPU + Latency cost), you pre-establish multiple connections and reuse them.
I believe in seeing things with my own eyes, so I opened Wireshark and captured actual packets.
1. 192.168.0.2 -> 8.8.8.8 [SYN] Seq=0
2. 8.8.8.8 -> 192.168.0.2 [SYN, ACK] Seq=0 Ack=1
3. 192.168.0.2 -> 8.8.8.8 [ACK] Seq=1 Ack=1
The Seq=0 here is Wireshark's convenience conversion to Relative Sequence Numbers. In reality, it's a random large number like 39281912.
If the server port is closed, the server immediately sends a RST (Reset) packet upon receiving SYN.
1. Client -> Server [SYN]
2. Server -> Client [RST, ACK]
In Java, this throws java.net.ConnectException: Connection refused. I've seen this error countless times while developing, and now I immediately know "Oh, the server process isn't running."
If a firewall in front of the server (like AWS Security Group) drops the packet, the server doesn't respond (silent).
1. Client -> Server [SYN]
2. (no response...)
3. Client -> Server [SYN] (Retransmission)
4. (no response...)
The client retries a few times and eventually throws SocketTimeoutException. If the error message is "Refused," it means "server is on but program isn't running." If it's "Timeout," it's 99% likely a "network/firewall issue." Knowing this difference made troubleshooting much faster.
As a developer, all we can control in code is connect() and close(), but inside the OS kernel, complex state transitions are happening.
ServerSocket)connect()If it gets stuck in step 3 (SYN_RCVD), that's a SYN Flooding attack. Hackers send countless SYNs without sending ACKs, filling up the server's backlog queue in a DDoS attack.
If it stays in CLOSE_WAIT state? That's a Code Leak. You didn't call input.close(). When I first built my service, I made this mistake and watched the server slow down and eventually die.
After the handshake, do we immediately pour out data? No. We need to check the receiver and network conditions.
All of this happens automatically inside TCP (kernel). When we call socket.send(data), the OS performs all these complex algorithms and chunks the data. UDP doesn't have this, so developers have to implement it themselves.
3-Way Handshake is safe but slow. It wastes 1 RTT (Round Trip Time) every time. Google proposed TCP Fast Open (RFC 7413).
The idea is simple. "We talked yesterday, can we skip the greeting?" On the first connection, you get a Cookie. From the second connection onwards, you send Data + Cookie inside the SYN packet. The server verifies the cookie and processes data even before the handshake completes. It's widely used in modern browsers and Linux servers.
TCP strictly maintains data order (1, 2, 3). If packet 2 is lost? Even if 3, 4, 5 have already arrived, the OS won't give them to the application and waits tightly until packet 2 is retransmitted. The whole stream freezes.
This is why HTTP/3 (QUIC) abandoned TCP and chose UDP. UDP doesn't have order obsession, so streams can flow independently. At first I wondered "Why use UDP?" but understanding this problem made it clear.
Studying Linux kernel parameters (sysctl.conf), I found that environments handling large traffic often need to go beyond the default TCP settings. These are the settings worth knowing about.
When SYN packets flood in, the kernel's backlog queue fills up and even legitimate connections get refused.
# Check SYN backlog size
sysctl net.ipv4.tcp_max_syn_backlog
# 128 (default) -> need to increase to 4096 etc.
Enable cookie technology to defend against SYN Flooding attacks.
sysctl -w net.ipv4.tcp_syncookies=1
The TCP header's Window Size field is limited to 16 bits (65KB). That was enough in the old days, but in the gigabit era, it's woefully insufficient. Enabling Window Scaling allows window sizes up to 1GB through shift operations.
sysctl -w net.ipv4.tcp_window_scaling=1
These settings are typically tuned when operating high-performance middleware like Redis or Kafka.
Sending FIN in 4-Way Handshake means "I have no more data to send," not "I won't listen." Using this subtle difference is Half-Close.
socket.close(): Closes both read/write streams. If the other side sends more data, it errorssocket.shutdown(SHUT_WR): Closes only "write" (FIN sent). But you can still read data from the other sideThink about a client uploading a file to a server. The client finishes sending the file and calls shutdown(WR) to send FIN. But the server might not have sent the "file received successfully" response yet. The client should close only writing and maintain the reading state (reading open) to wait for the server's response. This is the proper way to do graceful shutdown.
When I first learned TCP 3-Way Handshake, I thought "It's just 3 greetings." But the deeper I dug, the more I realized how precisely designed this protocol is.
These 3 steps allow us to have reliable communication even on unreliable networks. They prevent zombie packet problems, guarantee bidirectional communication, and exchange sequence numbers to maintain data order.
When debugging network errors in my service, properly understanding this concept allowed me to find problems much faster. The difference between "Refused" and "Timeout," the meaning of TIME_WAIT, the need for Connection Pools... all of these naturally connected once I understood 3-Way Handshake.
In the end, TCP is a "polite protocol." It listens before speaking, and considers the other party before disconnecting. Understanding this philosophy makes network programming much easier.