What is it? #
A port is a number that identifies which program on a machine should receive incoming traffic.
Only one program can listen on a given port and address at a time, which is why starting a second copy of a service fails with "address already in use".
The binding address decides who can reach it. Binding to 127.0.0.1 accepts only local connections; binding to 0.0.0.0 accepts connections from anywhere the network and firewall allow.
Most connection problems come down to four questions: is anything listening, on which address, does the firewall allow it, and is the name resolving correctly.
Think of it like this #
Doors in a building, each numbered. A delivery for door 443 goes to whoever is standing behind it.
Two people cannot stand behind the same door. And a door that opens only onto an internal corridor cannot be used from the street, however much you knock.
Simple example #
Your application is running but the browser says connection refused. You need to determine whether it is listening, on what address, and whether anything between you and it is blocking the traffic.
Code #
# What is listening on this machine?
sudo ss -tulpn
# -t TCP -u UDP -l listening -p process -n numeric (no name lookups)
# Example output
# tcp LISTEN 0 511 0.0.0.0:80 users:(("nginx",pid=812,fd=6))
# tcp LISTEN 0 511 127.0.0.1:8000 users:(("gunicorn",pid=1043,fd=5))
# tcp LISTEN 0 244 127.0.0.1:5432 users:(("postgres",pid=640,fd=5))
# └── the binding address is the important part
# Who is using a specific port?
sudo ss -tulpn | grep :8000
sudo lsof -i :8000
# Can I reach a port from here?
nc -zv example.com 443
curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:8000/health
Binding addresses
127.0.0.1:8000 only this machine — correct for an app behind a proxy
0.0.0.0:80 every interface — correct for the reverse proxy itself
10.0.1.5:5432 one private interface — correct for a database on a
private network that other servers must reach
[::]:80 the IPv6 equivalent of 0.0.0.0
Well-known ports
22 SSH 80 HTTP 443 HTTPS
25 SMTP 53 DNS 123 NTP
3306 MySQL 5432 PostgreSQL 6379 Redis
27017 MongoDB 9200 Elasticsearch 3000/8000 common app ports
Below 1024 requires privilege to bind, which is why web servers
start as root and then drop to an unprivileged user.
Diagnosing "connection refused" in order
1. is the process running? systemctl status myapp
2. is it listening, and where? sudo ss -tulpn | grep 8000
3. can it be reached locally? curl localhost:8000/health
4. does the firewall allow it? sudo ufw status
5. does the name resolve correctly? dig +short example.com
"Connection refused" means nothing is listening.
"Connection timed out" usually means a firewall is dropping the traffic.
How it works #
ss -tulpn is the modern replacement for netstat. The -p flag shows the owning process, which is what turns "something is on port 8000" into "gunicorn, PID 1043".
The address column is the part people skim past and should read carefully. An application on 127.0.0.1:8000 is invisible from other machines by design; if you expected it to be reachable, the binding is the problem, not the firewall.
That local binding is usually correct. Applications should sit behind a reverse proxy, and binding to localhost means there is no direct public path to them at all.
Databases should bind to localhost or a private address. A PostgreSQL instance on 0.0.0.0 with a weak password is found by scanners within minutes.
The distinction between refused and timed out is the most useful diagnostic detail here. Refused means the connection reached the machine and nothing was listening. Timed out means the packets went nowhere — almost always a firewall or a security group.
Ports below 1024 need privilege, which is why services like Nginx start as root, bind port 80, and then drop to www-data for the actual work.
Real-world use #
The five-step diagnosis covers nearly every "I cannot reach the service" problem, and doing it in order saves a great deal of guessing.
In cloud environments there are usually two firewalls: the machine's own and the provider's security group. A timeout that persists after opening the local firewall is almost always the security group.
Port conflicts are common in development, where several projects want port 3000 or 8000. lsof -i :3000 identifies the culprit immediately.
In containers, the published port maps a host port to a container port, and a service bound to 127.0.0.1 inside a container is unreachable even when the port is published — the container's localhost is not the host's.
Auditing what is listening is a basic security check. Anything on 0.0.0.0 that does not need to be public should be changed or firewalled.
Common mistakes #
- Binding an application to 127.0.0.1 and expecting external access.
- Binding a database to 0.0.0.0 on a public server.
- Opening firewall ports before checking whether anything is listening.
- Ignoring the difference between connection refused and timed out.
- Forgetting the cloud provider’s security group as well as the host firewall.
Practice #
On a machine you control, list every listening socket and note the binding address for each. Identify anything bound to all interfaces that should not be. Then start a service on localhost only and confirm from another machine that it is unreachable.