When I first started working with web servers, one thing confused me: how can multiple browser tabs communicate simultaneously through port 80 while only one process can bind to that port? The answer lies in how TCP connections are established.
Every TCP connection is uniquely identified by four values:
Source IP : Source Port → Destination IP : Destination Port
192.168.1.5:54321 → 93.184.216.34:80
192.168.1.5:54322 → 93.184.216.34:80
The web server listens on port 80, but each client connection comes from a different ephemeral port (usually between 32768-60999). This allows multiple connections to the same destination port.
Let's create a simple server that shows active connections:
import socket
import threading
def handle_client(conn, addr):
print(f"Connection from {addr[0]}:{addr[1]}")
conn.send(b"HTTP/1.1 200 OK\r\n\r\nHello from port 80!")
conn.close()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost', 80))
server.listen(5)
print("Server listening on port 80...")
while True:
conn, addr = server.accept()
threading.Thread(target=handle_client, args=(conn, addr)).start()
Only one process can bind to a specific port at a time because the operating system needs to know which process should receive incoming packets. When you try to start a second server on port 80, the OS rejects it because:
- The port is already marked as "in use"
- There's no way to determine which process should handle new connections
This explains why:
- Load balancers can accept many connections on port 80/443
- Web servers like Nginx can handle thousands of simultaneous requests
- You can't run Apache and Nginx on the same port simultaneously
Modern systems do allow limited port sharing with SO_REUSEPORT:
# Linux kernel 3.9+ feature
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s.bind(('0.0.0.0', 80))
This enables multiple processes to bind to the same port, with the kernel load balancing between them.
When multiple browser tabs connect to a web server on port 80, they're all using the same server port, but each connection is uniquely identified by a combination of four values:
Client IP : Client Port → Server IP : Server Port 192.168.1.10:54321 → 93.184.216.34:80 192.168.1.10:54322 → 93.184.216.34:80
The operating system distinguishes connections using socket pairs (client IP+port and server IP+port). This is why multiple tabs can coexist on port 80:
- Each tab gets a random ephemeral port (typically 49152-65535)
- The server's TCP stack maintains separate connection states
- NAT devices rewrite the client port for external traffic
You can't have two processes listening on the same port because:
// This will fail if port 80 is already bound server1 = socket.listen(80); server2 = socket.listen(80); // ← Throws "Address already in use"
The OS prevents this to avoid ambiguity in packet routing – there'd be no way to determine which process should receive incoming packets.
Observe how the OS handles multiple connections:
# Terminal 1: Start listening server nc -l 8080 # Terminal 2: First client connection nc localhost 8080 # Terminal 3: Second client connection (works simultaneously) nc localhost 8080
Check the established connections with:
netstat -tulnp | grep 8080
Here's a basic Python server handling concurrent connections:
from socket import socket, AF_INET, SOCK_STREAM def handle_client(conn, addr): print(f"Connection from {addr}") conn.send(b"HTTP/1.1 200 OK\r\n\r\nHello from tab using port %d" % addr[1]) conn.close() server = socket(AF_INET, SOCK_STREAM) server.bind(('', 80)) server.listen(5) while True: conn, addr = server.accept() handle_client(conn, addr)
Modern browsers optimize port usage with HTTP keep-alive:
GET /page1 HTTP/1.1 Host: example.com Connection: keep-alive GET /page2 HTTP/1.1 Host: example.com Connection: keep-alive
The same TCP connection handles multiple requests sequentially, reducing port consumption.
When facing "Address already in use" errors:
- Check existing listeners:
sudo lsof -i :80
- Enable SO_REUSEADDR when appropriate
- Wait for TCP's TIME_WAIT state to expire (typically 60 seconds)