An IP address finds the machine. It cannot find the program. A server that hosts a website, a database, and an SSH daemon on one address needs a second number to steer each arriving packet to the right service — and a model for what "talking" means once the packet arrives.
Port numbers steer packets to services
A port is a 16-bit number (0–65535) that identifies a specific service on a machine. While an IP address identifies which machine, a port identifies which service on that machine. Every TCP and UDP segment carries a source and destination port alongside the addresses, so the operating system can demultiplex thousands of simultaneous conversations to the correct process.
- Well-known ports (0–1023): HTTP (80), HTTPS (443), SSH (22), DNS (53). Binding these requires privileges on most systems, which is why development servers use 3000 or 8080 instead.
- Registered ports (1024–49151): PostgreSQL (5432), Redis (6379), MySQL (3306). Conventional, not enforced — nothing stops you from running HTTPS on port 8443.
- Ephemeral ports (49152–65535 in the IANA registry; Linux's default working range is
ip_local_port_range32768–60999): chosen automatically by the OS for outbound connections. Each connection your browser opens consumes one until it closes, which caps a single client IP at roughly 28,000 simultaneous connections to one server address and port on Linux.
Sockets are the operating system's conversation endpoints
A socket is an endpoint for communication — identified by the tuple (protocol, local IP, local port, remote IP, remote port). For a TCP connection, that four-value combination (plus protocol) uniquely identifies one conversation, which is why a server can hold 50,000 browser sessions on port 443 at once: the remote half of each tuple differs.
A server socket binds to a local IP and port (e.g., 0.0.0.0:8080) and listens for incoming connections. When a client connects, the OS creates a new socket for that specific connection while the original keeps listening — accept-then-fork, repeated tens of thousands of times. The client side gets its own socket with an ephemeral local port, so the reply path is already addressed before the first byte of application data moves.
Connections open with three packets and close with four
The handshake that opens a connection
- SYN — The client sends a packet with the SYN flag set, choosing an initial sequence number (a random 32-bit value, not zero, so stale packets from old connections cannot be confused with new ones).
- SYN-ACK — The server acknowledges the client's SYN and sends its own SYN with its sequence number.
- ACK — The client acknowledges the server's SYN.
After the handshake, both sides know the other is ready and agree on initial sequence numbers. The price is one full round trip before any application data moves — 80 ms of setup on an 80 ms-RTT link, paid on every cold connection.
The exchange that moves data
The client and server exchange data. TCP ensures:
- Reliability: lost packets are retransmitted (detected by missing ACKs or duplicate ACKs).
- Ordering: out-of-order packets are reassembled using sequence numbers.
- Flow control: the receiver advertises a window size to prevent buffer overflow.
- Congestion control: the sender adjusts its rate based on network conditions (e.g., TCP Reno, BBR).
The handshake that closes a connection
- FIN — The initiator sends a FIN to close its side.
- ACK — The peer acknowledges the FIN.
- FIN — The peer sends its own FIN.
- ACK — The initiator acknowledges the peer's FIN.
This four-way handshake allows each direction to close independently (half-close). The side that sends the final ACK then waits through TIME_WAIT — twice the maximum segment lifetime, typically 60 seconds — holding the port so delayed duplicates cannot corrupt a later connection that reuses the same tuple. Under heavy churn that wait is visible: thousands of sockets pile up in TIME_WAIT and ephemeral ports run dry.
IP delivers packets, and TCP builds reliability on top
IP (the network-layer protocol) only provides best-effort delivery — it does not guarantee that a packet arrives, that it arrives only once, or that it arrives in order. TCP builds these guarantees on top of IP with sequence numbers, acknowledgments, and retransmission timers, which is why TCP connections are more expensive (handshake overhead, per-packet ACKs, retransmission buffers) but also more useful for applications that need reliability.
The division of labor is the whole point: IP routers stay simple and stateless, forwarding each packet independently, while exactly the two endpoints that care about correctness keep the state. UDP applications that need reliability — QUIC, game netcode, live media — reimplement the same machinery themselves rather than paying for TCP's in-order head-of-line blocking.
Where this bites in production
- "Connection refused" versus timeout. Refused means nothing listens on that port — check the bind address and port. A timeout means packets die silently — check firewalls and security groups. The fix lives in opposite places.
- TIME_WAIT pileups. Load tests that open 10,000 connections per minute from one client IP exhaust ephemeral ports even though the server is idle. Reuse connections or widen the ephemeral range before blaming the server.
- Binding to localhost by accident. A service bound to
127.0.0.1:8080is unreachable from other machines;0.0.0.0:8080listens on every interface. Half of all "it works locally but not in Docker" mysteries end here.