IPportswell-knownephemeral4-tuple5-tuple

Ports: Addressing a Process, Not a Machine

An IP address reaches a machine; a 16-bit port reaches one program on it — 203.0.113.10:443 names the HTTPS listener. The 4-tuple of both addresses and both ports identifies a connection, which is why one server port serves thousands of clients and why a client that opens too many connections to one destination runs out of ports.

ConceptualLinux
▶ InteractiveInterview question
Progress

The problem

A packet arrives at 203.0.113.10. The machine runs a web server, an SSH daemon, a database and forty client connections. IP has delivered the packet to the right machine — which process gets it?

Two levels of addressing

IP addresses a host — strictly, an interface. Once the packet is inside the host, the transport layer needs a second address to pick a socket, and that is the port: a 16-bit number (0–65535) in the TCP or UDP header, one for the source and one for the destination. 203.0.113.10:443 therefore means "the process that bound port 443 on the host at 203.0.113.10"; 203.0.113.10:22 on the same machine is a different process entirely. Ports are per transport: TCP 53 and UDP 53 are independent, and DNS listens on both.

A server binds a port and listens so that the kernel knows where to deliver new connections: bind(0.0.0.0:443) then listen(). A client does not choose its port; when it calls connect(), the kernel picks an ephemeral source port from a configured range, which becomes the address replies come back to. A port is not a resource on the network — routers do not see it, and only the two end hosts (and stateful middleboxes such as NATs and firewalls, which is what makes them stateful) care what it is.

Which numbers mean what

Linux

Well-known ports (0–1023) are assigned by IANA to specific services, and on Unix-like systems binding one requires root or the CAP_NET_BIND_SERVICE capability — which is why a development server defaults to 8080 or 3000 and why production containers often run unprivileged behind a proxy that owns 80 and 443. Registered ports (1024–49151) are also IANA-listed by convention (5432 PostgreSQL, 6379 Redis, 3306 MySQL, 27017 MongoDB, 9092 Kafka) but any process may bind them. Dynamic / ephemeral ports (49152–65535 per IANA) are for clients; Linux uses a wider range, 32768–60999 by default (net.ipv4.ip_local_port_range), giving 28,232 source ports; Windows and macOS use 49152–65535, about 16,000.

The numbers are conventions, not laws: a web server on port 8443 is still a web server, and a firewall that "allows only HTTPS" by allowing port 443 allows any protocol someone chooses to run there. What is fixed is that the client has to know the port in advance — it is in the URL scheme (https → 443), in the connection string, or in a DNS SRV record.

Ports to recognise on sight
PortServiceTransportNote
22SSHTCP
53DNSUDP + TCPTCP for large answers and zone transfers
67 / 68DHCPUDPserver / client
80HTTPTCP
443HTTPSTCP; UDP for HTTP/3 (QUIC)same number, different transport
123NTPUDP
3306 / 5432MySQL / PostgreSQLTCPregistered range
6379RedisTCP
8080 / 3000dev HTTPTCPunprivileged alternatives to 80
32768–60999ephemeral (Linux)bothclient source ports
49152–65535ephemeral (IANA, Windows, macOS)both

The 4-tuple: what a connection is

Linux

A TCP connection is identified by four values: (source IP, source port, destination IP, destination port). Add the protocol and it is the 5-tuple that NATs, firewalls and load balancers hash. The server’s side of every connection is the same — 203.0.113.10:443 — and that is fine, because the *client* side differs: 198.51.100.7:62014, 198.51.100.7:62015, 192.0.2.9:41000. The kernel demultiplexes an incoming segment by looking up the full 4-tuple in a hash table of established sockets; only a SYN that matches nothing falls through to the listening socket on port 443.

This answers the question beginners ask: how can one port serve thousands of clients? Because the port is not the connection; the tuple is. A server on port 443 can, in principle, hold 2^16 connections from *each* client IP, and accept() returns a *new* descriptor for each one while the listening socket keeps its port. It also answers the opposite question — why a client runs out: from one source IP to one destination IP:port, only the source port varies, so there are at most ~28,000 (Linux) distinct connections, and a closed connection’s tuple is unusable for the TIME_WAIT period (60 s on Linux) on the side that closed first.

`ss -tn` on a server: one listener, many tuples (educational model)
State    Recv-Q Send-Q Local Address:Port   Peer Address:Port
LISTEN   0      4096   0.0.0.0:443          0.0.0.0:*
ESTAB    0      0      203.0.113.10:443     198.51.100.7:62014
ESTAB    0      0      203.0.113.10:443     198.51.100.7:62015
ESTAB    0      0      203.0.113.10:443     192.0.2.9:41000
TIME-WAIT 0     0      203.0.113.10:443     192.0.2.9:40998
                       └── same local port ──┘ └── different peer tuple ──┘

Ephemeral port exhaustion

Linux

A service that opens a new connection per request to one upstream — a database, a cache, an internal API behind one IP — burns one ephemeral port per request and leaves it in TIME_WAIT for 60 s after closing. At 500 requests per second that is 30,000 tuples in TIME_WAIT at any moment, more than the 28,232 Linux allows to one destination. The next connect() fails with EADDRNOTAVAIL ("Cannot assign requested address"), and the symptom in the application is intermittent connection errors to a perfectly healthy upstream, with the upstream’s own metrics showing nothing wrong.

The fixes are ordered by how much they address the cause. Connection pooling and keep-alive — reuse connections instead of opening one per request — removes the problem; see Connection Pooling and Keep-Alive and Connection Reuse. Widening ip_local_port_range to 1024–65535 roughly doubles headroom. net.ipv4.tcp_tw_reuse=1 lets the kernel reuse a TIME_WAIT tuple for a new *outgoing* connection when timestamps allow. Spreading upstream traffic across several destination IPs multiplies the tuple space. NAT gateways hit the same wall from the other side — all clients share one source IP — which is why a NAT gateway’s connection-per-destination limit is a number in every cloud provider’s documentation.

  • Symptom: EADDRNOTAVAIL / "Cannot assign requested address" on connect(), or a NAT gateway’s "port allocation errors" metric climbing.
  • Evidence: ss -tan state time-wait | wc -l in the tens of thousands, almost all to one peer.
  • Cause: a connection per request. Fix: a pool. The sysctls are relief, not cure.

Key points

  • IP names the host; the port names the process (socket) on it. 203.0.113.10:443 is the HTTPS listener on that host.
  • Well-known 0–1023 (privileged on Unix), registered 1024–49151 by convention, ephemeral for clients: Linux 32768–60999, IANA/Windows/macOS 49152–65535.
  • A connection is the 4-tuple; the 5-tuple adds the protocol. Demultiplexing is a hash lookup on it; only unmatched SYNs reach the listener.
  • One server port serves unbounded clients because the client side of the tuple differs. accept() returns a new descriptor per connection; the listener keeps its port.
  • A client talking to one destination has ~28,000 source ports and each closed connection holds one in TIME_WAIT for 60 s. Connection-per-request exhausts them; pooling is the fix.
  • Ports are conventions the two ends agree on; routers never see them, NATs and firewalls do.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why a second address instead of one big one?

IP is routed by the network and must aggregate; process identity is local to a host and changes every second. Keeping the port in the transport header lets routers ignore it and lets a host renumber its processes without telling anyone.

Why can one port hold many connections?

Because delivery is by tuple, not by port. The listening socket is a factory; each accepted connection is a separate socket with the same local port and a distinct peer.

Why does `TIME_WAIT` hold a port for a minute?

To absorb late segments from the old connection so they cannot be mistaken for the new one on the same tuple, and to retransmit the final ACK if the peer’s FIN is lost. Two maximum segment lifetimes was the estimate; 60 s is Linux’s constant.

Ports and connections

Ports: one server port, many connections
A connection is a 4-tuple. The server reuses 443 for everyone; what makes each connection unique is the client's ephemeral port.
well-known 0–1023root/CAP_NET_BIND
registered 1024–49151IANA: 5432, 6379, 8080 …
dynamic 49152–65535IANA ephemeral
Linux picks ephemeral ports from net.ipv4.ip_local_port_range = 32768 60999 (28K ports), overlapping the IANA “registered” block.
203.0.113.10 listening
LISTEN  0.0.0.0:22  sshd
LISTEN  0.0.0.0:80  nginx
LISTEN  0.0.0.0:443  nginx
LISTEN  0.0.0.0:5432  postgres
connection table (ss -tn)
state  local              peer                  client
ESTAB  203.0.113.10:443   198.51.100.21:35385   laptop
ESTAB  203.0.113.10:443   198.51.100.21:34696   laptop
ESTAB  203.0.113.10:22    198.51.100.21:50613   laptop
ESTAB  203.0.113.10:5432  198.51.100.77:41392   app server
ESTAB  203.0.113.10:5432  198.51.100.77:41914   app server
ESTAB  203.0.113.10:80    192.0.2.9:38817       crawler
ESTAB  203.0.113.10:443   192.0.2.9:47224       crawler
Simulated

How it fails

What the failure looks like from inside real software.

  • EADDRINUSE on startup: something already listens on the port — often the previous instance still in TIME_WAIT; SO_REUSEADDR is the standard answer for servers.
  • EACCES binding port 80 as a non-root user: use a port ≥ 1024, CAP_NET_BIND_SERVICE, or a reverse proxy.
  • EADDRNOTAVAIL on connect(): ephemeral ports to that destination exhausted; tens of thousands of TIME_WAIT sockets confirm it; pool connections.
  • Connection refused on the right host: nothing listening on that port (ss -tln shows no LISTEN), or the service bound 127.0.0.1 rather than 0.0.0.0.
  • A firewall that "allows 443" passes a non-HTTPS tunnel on 443; the port is not the protocol.
  • HTTP/3 blocked by a UDP 443 rule while TCP 443 is open: browsers fall back silently and the site is merely slower.