TCP stands for Transmission Control Protocol, and its job is to make an unreliable network look like a reliable ordered stream of bytes — which is why almost everything uses it, and why the places where that illusion leaks explain a specific and recognisable set of production bugs.
The definition is the easy part and every glossary has it. The useful part is understanding four of its behaviours, because they show up constantly in real systems: connections that hang instead of failing, transfers that are slow despite plenty of bandwidth, servers that cannot bind a port after restarting, and the difference between a connection being refused and a connection being dropped.
Table of contents
- What TCP guarantees, and what it does not
- The handshake, and refused versus dropped
- Why connections hang instead of failing
- Slow transfers that are not a bandwidth problem
- TIME_WAIT, and why the port is still busy
- How this fits the rest of the stack
- FAQ
What TCP guarantees, and what it does not
TCP sits above IP, which delivers individual packets with no promises — they may arrive out of order, duplicated, or not at all. TCP builds a usable abstraction on that.
What it guarantees:
- Delivery, by acknowledging received data and retransmitting what is not acknowledged.
- Ordering, by numbering bytes so the receiver can reassemble them correctly.
- Integrity, via a checksum that detects corruption in transit.
- Flow control, so a fast sender does not overwhelm a slow receiver.
- Congestion control, so senders back off when the network is saturated.
What it does not guarantee, and this is the part people assume:
- Message boundaries. TCP is a byte stream. Two
send()calls may arrive as onerecv(), or one may arrive split across several. Any protocol on top must frame its own messages — with a length prefix or a delimiter. Assuming one send equals one receive is the most common TCP-related application bug there is. - Security. No encryption, no authentication. That is what TLS is for.
- Timeliness. Data will arrive, eventually. Retransmission means late data rather than lost data, which is why TCP is a poor fit for live audio and video where late is the same as lost.
- That the other end is still there. An idle TCP connection sends nothing, so a peer that vanished without closing is indistinguishable from one that is simply quiet.
That last one is the source of the most confusing class of failure, and it is worth its own section below.
The handshake, and refused versus dropped
A connection opens with three packets: the client sends SYN, the server replies SYN-ACK, the client sends ACK. One round trip before any data moves — which is why connection setup latency matters, and why connection pooling is worth doing.
What happens when it fails tells you a great deal, and the distinction is diagnostically valuable:
- Connection refused — the host replied with RST. Something is there and nothing is listening on that port. This is fast, and it is good news: the network path works.
- Connection timed out — no reply at all. The SYN went into a void. Something is silently dropping packets, typically a firewall configured to DROP rather than REJECT. This is slow, because the client retries with increasing delays before giving up.
- No route to host — the network layer could not even route the packet.
So a fast failure means the packets arrived and were rejected; a slow failure means they disappeared. That single observation eliminates half the possible causes of a connectivity problem before you look at anything else.
It is also why firewalls default to DROP for hostile traffic: it costs the scanner time and reveals nothing, whereas REJECT confirms the host exists.
Why connections hang instead of failing
This is the behaviour that produces the worst outages, because a hang is much harder to handle than an error.
An established TCP connection with no data flowing sends nothing at all. If the other end crashes, is killed, or has its network cut, your end has no idea. It is holding a connection to something that no longer exists, and it will wait indefinitely for a response that will never come.
In an application this looks like a request that never returns. Threads or connections accumulate, each waiting on a dead socket, until the pool is exhausted and the service stops accepting work — an outage caused by something that failed silently somewhere else.
There are three defences and you want all of them:
- TCP keepalive, which sends a probe after a period of idleness and tears the connection down if unanswered. The OS defaults are famously long — commonly two hours before the first probe — so set them per-socket rather than relying on them.
- Application-level timeouts, which are the important one. Every network call needs a deadline: connect timeout, read timeout, and total request timeout. A client library with no timeout configured usually means no timeout at all.
- Application-level heartbeats for long-lived connections, so both ends can detect a dead peer within seconds rather than hours.
If you take one operational habit from this: set explicit timeouts on every outbound call. The default in a great many libraries is to wait forever, and forever is a long time during an incident.
Slow transfers that are not a bandwidth problem
TCP limits how much unacknowledged data can be in flight — the window. The maximum throughput of a connection is roughly the window size divided by the round-trip time, regardless of how much bandwidth is available.
The consequence catches people transferring data over long distances. A connection with a 64KB window and a 200ms round trip tops out around 2.6 Mbps no matter how fast the link is. You can have a gigabit connection at both ends and still transfer at a few megabits, because the sender spends most of its time waiting for acknowledgements.
This is the bandwidth-delay product, and it explains why:
- A transfer between continents is far slower than the same transfer within one.
- Running several parallel connections is faster than one — each has its own window.
- Window scaling exists, and matters. It is negotiated in the handshake and allows windows far beyond 64KB. It is on by default on modern systems, but a middlebox that mangles TCP options can silently disable it, producing exactly this symptom.
- Moving the origin closer to the user helps more than adding bandwidth.
There is also slow start: a new connection begins conservatively and ramps up. Short connections never reach full speed, which is another reason connection reuse matters — a pooled connection is already warmed up, while a fresh one starts from the bottom every time.
TIME_WAIT, and why the port is still busy
Closing a TCP connection takes four packets, and the side that closes first enters a state called TIME_WAIT, where it waits for twice the maximum segment lifetime — typically 60 seconds on Linux — before releasing the socket.
It exists for two real reasons: to absorb any delayed packets from the closed connection so they cannot be misinterpreted as belonging to a new one on the same port pair, and to ensure the final ACK can be retransmitted if lost.
Its visible consequence is the error every developer has hit: restart a server and it cannot bind its port because the old socket is still in TIME_WAIT. The correct fix is SO_REUSEADDR on the listening socket, which most frameworks set for you and which is safe for this purpose.
The less obvious consequence appears at scale. A service making many short-lived outbound connections accumulates thousands of sockets in TIME_WAIT, and can exhaust the ephemeral port range — at which point new outbound connections fail entirely on a machine that looks completely healthy.
The right fix is connection pooling and keep-alive: reuse connections instead of opening and closing them constantly. The tempting fix is net.ipv4.tcp_tw_reuse or widening the port range, and those are mitigations rather than solutions — tcp_tw_recycle in particular was removed from Linux because it broke connections from clients behind NAT. Fix the churn, not the symptom.
How this fits the rest of the stack
Most of what is above turns into two practical habits — set explicit timeouts on every outbound call, and reuse connections rather than churning them — and both interact directly with how many connections your database will accept. The RunxBuild hosting calculator shows the database beside the service, storage and bandwidth so those limits are chosen deliberately. RunxBuild managed MySQL and Postgres expose configurable connection limits and sit on private networking, which keeps pool sizing an explicit decision rather than something discovered under load.
Useful related references:
- TCP Port Syslog: 514, 6514 (TLS), and the rsyslog/syslog-ng Setup
- 443 Port TCP: HTTPS, TLS Handshake, and Firewall Rules
- TCP Port VNC: 5900, 5901, 5902, and the Display-Number Pattern
- Services on RunxBuild
FAQ
What does TCP stand for?
Transmission Control Protocol. It runs on top of IP and turns unreliable packet delivery into a reliable, ordered byte stream, adding acknowledgement and retransmission, ordering, integrity checking, flow control and congestion control.
Does TCP preserve message boundaries?
No, and assuming it does is the most common TCP-related application bug. TCP is a byte stream — two sends may arrive as one receive, or one send may arrive split across several. Any protocol on top must frame its own messages with a length prefix or a delimiter.
Why does my connection hang instead of failing?
Because an idle TCP connection sends nothing, so if the peer crashes or loses its network your end has no way to know and waits indefinitely. Set explicit application-level timeouts on every network call, enable TCP keepalive with sensible values rather than the multi-hour OS defaults, and use heartbeats on long-lived connections.
What is the difference between connection refused and connection timed out?
Refused means the host replied with a reset — something is there and nothing is listening on that port, and the network path works. Timed out means no reply at all, so something is silently dropping packets, usually a firewall set to DROP rather than REJECT. Fast failure means rejected; slow failure means disappeared.
Why is my transfer slow despite having plenty of bandwidth?
Throughput is limited by window size divided by round-trip time, not by link capacity. A 64KB window over a 200ms round trip caps out around 2.6 Mbps regardless of bandwidth. Window scaling normally raises this, but a middlebox that mangles TCP options can silently disable it. Parallel connections and moving the origin closer both help.