Ports, Sockets, and Multiplexing
IP gets a packet to a host. It says nothing about which of the hundreds of processes on that host should receive it. The transport layer closes that gap with multiplexing: a 16-bit port identifies a process-level endpoint, and the kernel uses it to hand each arriving segment to exactly one program. This is the seam where the network stops and your application begins — below it the kernel forwards bytes, above it your code reads a stream.
A socket is the operating system's object for that endpoint: a handle bound to a protocol and an (IP, port) pair. A server binds a socket to a port and listens; a client opens a socket and connects. The kernel keeps a table of every active connection and, for each incoming packet, looks up the one socket it belongs to. Get the mapping rule wrong — assume a port can serve only one connection, or run a busy client out of source ports — and you will misread a whole class of production failures.
Port Ranges and Their Conventions
A port is a 16-bit number, so the range is 0–65535, split into three zones by convention. Well-known ports below 1024 are reserved for standard services — 22 for SSH, 443 for HTTPS — and on Unix a process needs root or the CAP_NET_BIND_SERVICE capability to bind one. Registered ports from 1024–49151 are assigned to applications like PostgreSQL on 5432. The top range is the ephemeral pool the kernel draws from for outbound connections.
The destination port on an outbound connection is the server's well-known port; the source port is one the kernel picks from the ephemeral range, typically 32768–60999 on Linux. That source port is the per-connection handle that lets the client tell apart two simultaneous connections to the same server — and the pool's size is a hard ceiling on how many such connections one client can hold to one destination at once.
# the ephemeral source-port range the kernel allocates from sysctl net.ipv4.ip_local_port_range # net.ipv4.ip_local_port_range = 32768 60999 (~28k ports) # widen it on a busy outbound proxy: sysctl -w net.ipv4.ip_local_port_range="10000 65535"
Sockets — Listening versus Connected
A listening socket is bound to a local (IP, port) and does nothing but wait; it has no remote peer. When a client connects, the kernel completes the handshake and hands your accept() call a brand-new connected socket that records the full pair of endpoints — local and remote IP and port both. The listening socket stays put, ready for the next client, while the connected socket carries one conversation.
This is the detail that surprises people: a thousand clients talking to one web server all hit the same destination 443, yet the kernel keeps a thousand distinct connected sockets. They share a destination port but differ in the remote IP and remote port, so each is a unique connection. One listening port fans out into as many connected sockets as there are clients — the port is not the bottleneck the count of connections might suggest.
The 5-Tuple and Demultiplexing
The kernel does not identify a connection by destination port alone. It uses the 5-tuple: protocol, source IP, source port, destination IP, destination port. Every active TCP or UDP flow has a unique 5-tuple, and that tuple is the key the kernel hashes to find the right socket when a packet arrives. Change any one of the five values and it is, to the kernel, a different connection.
This is why one server port handles thousands of clients with no collision: the destination IP and port are fixed at 10.0.0.5:443, but each client contributes a different source IP and source port, making every tuple distinct. It is also why a single client can open many connections to the same server — the kernel just allocates a fresh source port for each, varying the one field it controls. The 5-tuple is the demultiplexing key for the whole transport layer.
# every row is one 5-tuple: local addr:port -> peer addr:port ss -tn state established # Local Address:Port Peer Address:Port # 10.0.0.5:443 203.0.113.7:51234 <- client A # 10.0.0.5:443 203.0.113.7:51235 <- client A, 2nd conn # 10.0.0.5:443 198.51.100.9:40021 <- client B
Ephemeral Port Exhaustion
Because the source port is the only field a client varies when connecting to a single destination, the ephemeral range caps how many simultaneous connections it can make to that one (dst IP, dst port). With ~28,000 ports available and connections lingering in TIME_WAIT for tens of seconds after close, a busy outbound proxy or a load-testing client can run the pool dry and start failing with EADDRNOTAVAIL — even though the network and the server are perfectly healthy.
The fix is rarely to widen the range alone. Reusing connections through keep-alive and pooling is what actually removes the pressure, because an amortized connection never returns its port to the pool in the first place. Widening ip_local_port_range and enabling tcp_tw_reuse buy headroom, but a client that opens a fresh connection per request will exhaust any range you give it under enough load.
A listening socket is bound to one local (IP, port) with no remote peer; its job is to sit in an accept queue and produce new sockets. A server has exactly one per port it serves, no matter how many clients arrive.
A connected socket records the full 5-tuple — both endpoints — and carries a single conversation. The kernel mints one per accepted client, which is how thousands of clients coexist on the one listening port: they share the local side and differ on the remote.
- Assuming one port equals one connection. The kernel keys on the full 5-tuple, so a single listening port carries as many connected sockets as there are distinct remote endpoints — capacity is bounded by memory and file descriptors, not by the port.
- Running a busy outbound client or proxy out of ephemeral ports. Open a fresh connection per request to one destination and the ~28,000-port pool drains, returning EADDRNOTAVAIL while the server sits idle and blameless.
- Binding a process to a port below 1024 without privilege. The bind fails with EACCES unless the process has root or CAP_NET_BIND_SERVICE — a frequent surprise when moving a service from a dev port to 443.
- Forgetting SO_REUSEADDR after a crash. A restarted server fails to rebind because the old socket lingers in TIME_WAIT, and the restart loops on EADDRINUSE until the timer expires.
- Picking an ephemeral-range port as a fixed service port. Bind a service to something inside 32768–60999 and the kernel may already have handed it to an outbound connection, causing intermittent bind failures that are maddening to reproduce.
- Reuse connections with keep-alive and pooling on any client that talks repeatedly to the same destination, so ports are amortized instead of consumed one-per-request.
- Set SO_REUSEADDR on listening sockets so a restarted server rebinds immediately instead of waiting out a TIME_WAIT on its own listening port.
- Reach for ss -tn over the legacy netstat to read live sockets by state; it is faster on hosts with tens of thousands of connections and shows the 5-tuple directly.
- Widen ip_local_port_range and enable tcp_tw_reuse on high-fan-out proxies, but only after pooling, since a wider range only delays exhaustion if connections are never reused.
- Grant CAP_NET_BIND_SERVICE to a service that must own a low port rather than running the whole process as root, keeping the privilege scoped to the one capability it needs.
Knowledge Check
How does one web server on port 443 keep thousands of client connections distinct?
- Each connection has a unique 5-tuple, differing in source IP and port
- The server hands each connecting client a different destination port number once the connection has been fully accepted
- A single connected socket multiplexes all the clients internally by sequence number
- The kernel opens a separate listening socket on port 443 for every client
A proxy making many short outbound connections to one backend starts failing with EADDRNOTAVAIL. What is the most likely cause?
- The backend server itself has finally run out of capacity and has begun actively refusing all of the new inbound connections
- The client exhausted its ephemeral source ports to that destination
- The kernel conntrack table on the proxy has filled and is dropping flows
- DNS resolution for the backend hostname has begun to fail intermittently
Why does a process need elevated privilege to bind to port 443 but not to port 8443?
- Port 443 requires kernel-level TLS termination, which only a sufficiently privileged process is permitted to configure and then bind
- Port 443 sits in the ephemeral range that the kernel reserves for itself
- Ports below 1024 are privileged and need root or CAP_NET_BIND_SERVICE
- Port 443 is registered to a named service, so only its registered owner may bind it
You got correct