An IP address gets a packet to the right machine — a port number gets it to the right process on that machine. Without ports, one server could only ever run one network service at a time.
3 min read
A single server might run a web app on one process, a database on another, and an SSH daemon on a third — all reachable at the same IP address. Ports are a 16-bit number (0–65535) that says which process on that machine a packet is actually meant for, layered on top of the IP address rather than replacing it.
A full network endpoint is really the pair: 93.184.216.34:443 means "this IP,
port 443 specifically" — and it's this pair (plus the equivalent pair on the
other end) that uniquely identifies one connection.
Ports below 1024 are "well-known/privileged" ports — on most operating systems, binding to one requires elevated permissions, which is part of why a random untrusted process can't silently impersonate a web server on port 80.
When your browser connects out to example.com:443, it doesn't use port 443 on
its own side — it picks a random high-numbered port (say, 52413) as its
source port. The full connection is identified by both ends' IP:port pairs:
Client: 192.168.1.5:52413 -------> Server: 93.184.216.34:443
This is exactly why one machine can hold thousands of simultaneous connections to the very same server: each one gets a different random client port, so each 4-tuple (source IP, source port, destination IP, destination port) is unique even though three of those four values are identical every time.
A socket is the operating system's handle for one end of a network connection — the object your code actually reads from and writes to. Opening a socket, in the shape every language's networking library reduces to underneath:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4, TCP
s.connect(("example.com", 443))
s.sendall(b"...") # write bytes to the connection
data = s.recv(4096) # read bytes back
s.close() # release the socket — same idea as closing a fileEvery HTTP client library (requests, fetch, axios) is doing exactly this
underneath, just wrapped in a much friendlier API that also handles TLS,
redirects, and parsing the response for you.
Only one process can bind to a given port on a given IP at a time. Restart a local dev server too quickly and you'll often hit this:
$ python manage.py runserver
Error: [Errno 98] Address already in useThe previous process is either still shutting down (the OS holds the port for a
short grace period even after a process exits, to let in-flight packets drain)
or is a genuinely stuck zombie process still holding it. lsof -i :8000 (Mac/
Linux) or netstat -ano | findstr :8000 (Windows) shows exactly what's still
holding a given port, so you can kill the right process instead of guessing.
Docker Compose's ports: "8000:8000" mapping, a .env file's PORT=3000, a
reverse proxy config routing /api to localhost:5000 — every one of these is
directly manipulating this exact concept. Understanding port binding is what
makes "why can't my container reach the database" or "why does my app work
locally but not when someone else runs it" debuggable instead of mysterious.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What uniquely identifies one TCP connection among thousands to the same server?
2. Why do ports below 1024 typically require elevated permissions to bind to?
3. What does 'Address already in use' most often indicate when restarting a local dev server?
Networking Fundamentals