Every TCP socket is always in one of a fixed set of states. Learn what each state means and when a socket enters it.
What Is a TCP State?
A TCP connection is not a simple on/off switch. Under the hood, the operating system models each TCP endpoint as a state machine. A state machine means the TCP can be in exactly one state at a time from a fixed list of states. When something happens — like an application calling connect(), or a SYN segment arriving from the network — the TCP moves from its current state to a new one. This movement is called a state transition.
On Linux, you can see the current state of all TCP sockets using:
# Show all TCP sockets and their states
ss -tan
# OR using the older netstat tool
netstat -tan
The output will show states like LISTEN, ESTABLISHED, TIME_WAIT, etc. This tutorial explains every one of them.
Quick Overview of All TCP States
States In Detail
CLOSED is a fictional/virtual state defined by RFC 793. It simply represents the situation where no TCP connection exists and no kernel resources are allocated for any connection. Before you call socket() and after a connection is fully cleaned up, the endpoint is conceptually in the CLOSED state.
Think of it as the “off” state of a light switch. No data, no connection, nothing running.
When a server calls listen() on a socket, that socket moves into the LISTEN state. The TCP is now passively waiting for any client to connect. The server is not trying to connect to anyone — it is sitting there, ready to accept whoever shows up.
You will commonly see many sockets in LISTEN state on a Linux server:
$ ss -tln
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 127.0.0.1:3306 0.0.0.0:*
Port 22 (SSH), port 80 (HTTP), and port 3306 (MySQL) are all in LISTEN state, ready for clients.
Code — How a server enters LISTEN state:
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
int main() {
int sfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
/* Socket enters LISTEN state here.
Backlog 5 = up to 5 half-open connections queued */
listen(sfd, 5);
printf("Server is now in LISTEN state on port 8080\n");
/* ... accept() would be called next ... */
return 0;
}
When a client calls connect(), the operating system immediately sends a SYN segment to the server and puts the socket in the SYN_SENT state. The client is now waiting for the server to reply with a SYN-ACK. This state is very brief under normal conditions — if the server is alive, the SYN-ACK arrives quickly and the state moves forward.
If you see a socket stuck in SYN_SENT for a long time, it usually means the server is unreachable, the firewall is dropping SYN packets, or the server’s port is not open.
/* Client side — connect() triggers SYN_SENT */
int cfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in srv = {0};
srv.sin_family = AF_INET;
srv.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.10", &srv.sin_addr);
/*
* At this point, the OS sends a SYN segment.
* Socket is in SYN_SENT until SYN-ACK is received.
* connect() blocks until the three-way handshake completes.
*/
connect(cfd, (struct sockaddr *)&srv, sizeof(srv));
/* If we reach here, handshake succeeded — socket is ESTABLISHED */
When the server (in LISTEN state) receives a SYN from a client, it automatically sends back a SYN-ACK segment and moves to SYN_RECV. The server is now waiting for the client’s final ACK to complete the three-way handshake.
Sockets in SYN_RECV are held in the incomplete connection queue (also called the SYN queue). If the final ACK never arrives, these entries age out. A SYN flood attack works by sending millions of SYNs and never sending the final ACK, filling up this queue.
After the three-way handshake completes, both sides move to the ESTABLISHED state. This is the state where data transfer happens. Both endpoints can now send and receive data freely. Most sockets you see in a running system will be in ESTABLISHED state — these are active HTTP connections, SSH sessions, database queries, etc.
/* Once connect() returns (client) or accept() returns (server),
the socket is in ESTABLISHED state and you can read/write freely */
char buf[1024];
/* Server: read data from client */
ssize_t n = read(cfd, buf, sizeof(buf));
if (n > 0) {
printf("Received: %.*s\n", (int)n, buf);
}
/* Server: send reply back */
const char *reply = "Hello from server!\n";
write(cfd, reply, strlen(reply));
These two states belong to the side that calls close() first — the active closer. Here is what happens:
- FIN_WAIT1: The application called
close(). TCP sends a FIN segment to the peer. Now waiting for an ACK from the peer for that FIN. - FIN_WAIT2: The peer acknowledged our FIN (sent ACK). But the peer has not yet sent its own FIN. So we are waiting for the peer’s FIN to arrive.
FIN_WAIT2 can last a long time if the peer application is slow to call close() on its end. Linux has a timeout for this (controlled by tcp_fin_timeout sysctl) to prevent sockets from sitting in FIN_WAIT2 forever.
/* Check tcp_fin_timeout (default 60 seconds on Linux) */
$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
CLOSING is an unusual state that only happens when both sides call close() at almost exactly the same time. Instead of the normal four-step sequence where one side closes first and the other closes second, both sides send FIN at the same time. When your TCP is in FIN_WAIT1 and instead of receiving an ACK you receive a FIN, you move to CLOSING.
You will almost never see this state in practice. It is a theoretical edge case defined to make the state machine complete.
TIME_WAIT is one of the most important and frequently misunderstood TCP states. After the active closer receives the peer’s FIN, it enters TIME_WAIT instead of immediately closing. It stays in TIME_WAIT for a period of 2 × MSL (Maximum Segment Lifetime).
There are two important reasons for this wait:
On Linux, MSL is 60 seconds, so TIME_WAIT lasts 120 seconds (2 minutes).
/* View TIME_WAIT sockets on a busy server */
$ ss -tan | grep TIME-WAIT | wc -l
3842
/* On high-traffic servers, thousands of TIME_WAIT sockets are normal.
They do not mean something is wrong. They clean themselves up. */
/* To reduce TIME_WAIT accumulation (use carefully): */
$ sysctl net.ipv4.tcp_tw_reuse=1
/* Allows reusing TIME_WAIT sockets for new outgoing connections
when it is safe to do so (only for outgoing connections) */
When the peer sends a FIN (meaning the peer is doing an active close), the local TCP acknowledges it and enters CLOSE_WAIT. This state means “we received a FIN from the peer but our application has not yet called close()”. The application can still send data to the peer during CLOSE_WAIT (this is called a half-close).
Common bug: If you see many sockets stuck in CLOSE_WAIT on a server, it usually means the server application has a bug — it received an end-of-file on the socket (the peer closed), but the application code forgot to call close() in response. These sockets will pile up and eventually exhaust file descriptors.
/* This bug causes CLOSE_WAIT accumulation */
int cfd = accept(sfd, NULL, NULL);
char buf[256];
while (1) {
int n = read(cfd, buf, sizeof(buf));
if (n == 0) {
/* Peer closed. We should call close(cfd) here! */
/* BUG: forgetting to close() leaves socket in CLOSE_WAIT */
break; /* We break but never close */
}
/* process data */
}
/* cfd is never closed — socket stays in CLOSE_WAIT forever */
/* FIX: always call close() when read() returns 0 */
if (n == 0) {
close(cfd); /* This moves socket out of CLOSE_WAIT to LAST_ACK */
break;
}
After the passive closer’s application calls close(), the TCP sends a FIN to the active closer and enters LAST_ACK. It is waiting for the active closer to acknowledge this FIN. Once that ACK arrives, the connection is completely done and kernel resources are freed. LAST_ACK is usually very brief.
Interview Questions
Answer: A TCP state machine models a TCP endpoint as always being in one of a fixed set of states. This is necessary because establishing and tearing down a TCP connection is a multi-step process involving coordination between two endpoints across a network. The state machine reduces complexity by clearly defining what a TCP should do when it receives a particular event (like a SYN, FIN, or ACK) based on its current state.
Answer: CLOSE_WAIT is entered by the passive closer — the side that received a FIN first. It means the peer has closed but our application has not called close() yet. TIME_WAIT is entered by the active closer — the side that closed first. It means the full close sequence is done, but the TCP waits 2×MSL to ensure reliable termination and to prevent old segments from corrupting new connections.
Answer: On a high-traffic server (like a web server handling many short HTTP connections), it is completely normal to see thousands of TIME_WAIT sockets. Each completed connection leaves behind a TIME_WAIT entry that lasts 2×MSL (120 seconds on Linux). At high request rates, they accumulate. This is generally not a problem — the kernel handles them efficiently. It only becomes a problem if you run out of local port numbers, in which case SO_REUSEADDR or tcp_tw_reuse can help.
Answer: CLOSE_WAIT accumulation is almost always a bug in the server application. When the peer closes the connection, the server’s read() returns 0 (end-of-file). If the server code does not detect this and call close() on the socket, the socket stays in CLOSE_WAIT indefinitely. Over time, this leaks file descriptors and can crash the server. The fix is to always call close() (or shutdown()) when read() returns 0.
Answer: The SYN queue (or incomplete connection queue) holds connections that are in the SYN_RECV state — connections where the server has received a SYN and sent a SYN-ACK but has not yet received the final ACK. When the final ACK arrives, the connection moves to ESTABLISHED and is placed in the accept queue (completed connection queue), from where the application retrieves it via accept().
