Ports

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: A number (0–65535) tagged onto an IP address that identifies WHICH program on that machine the traffic is for. An IP is the building; the port is the apartment number.


In plain English

A computer can run many programs at once that all want to talk to the network: a web server, an SSH server, an email server, a database, a dev server. They all share the same IP address. So how does incoming traffic know which one it’s for?

Ports. Each program “listens” on a specific port number (0-65535). Incoming traffic is addressed to IP:port, and the OS routes it to whichever program is listening on that port.

When you visit https://example.com, you’re actually visiting https://example.com:443 — port 443 is the default for HTTPS, so the browser hides it. Visit http://example.com:80 and you’re using port 80 (default HTTP). Run npm run dev and Next.js typically listens on port 3000 — visit http://localhost:3000.

A port-IP pair (76.76.21.21:443) uniquely identifies a SERVICE on a specific machine. Multiple services on the same machine just use different ports.

For typical webapp work, you encounter:

  • 80 — HTTP (default; almost always redirects to HTTPS)
  • 443 — HTTPS (default)
  • 3000 — Next.js / many Node dev servers
  • 5432 — Postgres
  • 6379 — Redis
  • 22 — SSH
  • 3306 — MySQL

This entry covers the protocol concept. For application-level usage (Next.js dev port, Vercel deployment), see What is hosting?.


Why it matters

Three concrete reasons knowing about ports pays off:

  1. localhost:3000 makes sense. Once you understand ports, “the port is busy” / “address already in use” errors become intelligible instead of mysterious.

  2. Network debugging. “Why can’t I reach my server?” can be: wrong port, port not open in firewall, another process holding the port, wrong protocol. Each is a different fix.

  3. Hosting concepts click. “Edge functions can run on port 80/443 because the platform manages those ports for you” makes sense once you know what ports ARE.

The trade-off: ports are a small concept with a few rules. Reading this entry once is enough; you won’t reference it weekly.


The numbering scheme

Ports are 16-bit unsigned integers — 0 to 65535. They split into three ranges:

RangeNameWhat’s here
0–1023Well-known (privileged)HTTP, HTTPS, SSH, FTP, etc. Requires admin/root to bind.
1024–49151RegisteredSpecific apps register here (MySQL 3306, Redis 6379, Postgres 5432). Can be bound by any user.
49152–65535Dynamic / ephemeralUsed for short-lived outbound connections.

The “well-known” privilege requirement is why you can’t run next dev on port 80 without sudo — port 80 needs root. Dev servers default to 3000+ to avoid that.


The well-known ports you’ll meet

PortProtocolWhat it is
21FTPFile Transfer Protocol (legacy)
22SSHSecure Shell — remote terminal access
25SMTPSending email
53DNSDomain Name resolution
80HTTPWeb (plain)
110POP3Reading email (legacy)
143IMAPReading email
443HTTPSWeb (encrypted)
465SMTPSEncrypted SMTP
587SMTPSubmission (sending email, modern)
993IMAPSEncrypted IMAP

For webapp work, only 80 and 443 matter day-to-day. Browsers default to them; almost no website uses anything else publicly.


Application / dev server ports

Common development ports:

PortService
3000Next.js (default), Create React App, Express
3001Next.js when 3000 is busy (auto-picks)
4200Angular CLI
5173Vite
5432Postgres
6379Redis
8000Django (default)
8080Tomcat, common alternate HTTP
8443Common alternate HTTPS
9000Various tools
27017MongoDB

For Bible Quest-style projects, you’ll see 3000 (Next.js) and 5432 (if you ever connect directly to Supabase Postgres) most often.


A concrete example: visiting a site

When you type https://example.com/about in the browser:

  1. Browser parses URL → host example.com, path /about, port (default for HTTPS) = 443
  2. DNS resolves example.com → 76.76.21.21
  3. Browser opens TCP connection to 76.76.21.21:443
  4. TLS handshake on port 443
  5. HTTP request sent over the encrypted connection
  6. Response received
  7. Page renders

For http://localhost:3000/:

  1. Browser parses URL → host localhost, path /, port 3000
  2. DNS resolves localhost → 127.0.0.1
  3. Browser opens TCP connection to 127.0.0.1:3000
  4. No TLS for plain HTTP
  5. HTTP request sent
  6. Whatever’s listening on 3000 (your next dev server) responds

The mechanism is identical; the destination port differs.


Listening vs connecting

A program can have two roles relative to ports:

  • Listening on a port — “I’m ready to accept incoming connections here.” Web servers, dev servers, databases all listen.
  • Connecting to a port — “I want to talk to whoever’s listening at that address.” Browsers, API clients, anything that initiates a request.

A typical Next.js dev server:

Listening on port 3000.
Browser connects: opens an ephemeral local port (e.g. 51234) → talks to 127.0.0.1:3000.

Each TCP connection has FOUR identifiers: (source IP, source port, destination IP, destination port). The ephemeral source port is automatic — the OS assigns one from the 49152-65535 range. You almost never set it manually.


What “Address already in use” means

When you start a server on port 3000 and get:

Error: listen EADDRINUSE: address already in use :::3000

Another process is already listening on port 3000. Maybe a leftover next dev from earlier. To fix:

Windows:

# Find what's on port 3000
Get-NetTCPConnection -LocalPort 3000
 
# Or older style
netstat -ano | findstr :3000
 
# Kill the process by PID
Stop-Process -Id <PID> -Force

Mac/Linux:

# Find what's on port 3000
lsof -i :3000
 
# Or
sudo netstat -tulpn | grep :3000
 
# Kill the process by PID
kill -9 <PID>

Or simply start your dev server on a different port:

# Next.js
PORT=3001 npm run dev
 
# Or
next dev -p 3001

In modern Next.js, if 3000 is busy, it automatically tries 3001, then 3002, etc.


Ports in hosting

In production webapp hosting, ports are mostly INVISIBLE to developers:

  • Vercel, Netlify, Cloudflare Pages — your function code doesn’t bind to a port. The platform handles HTTP/HTTPS routing on 80/443 for you. You just export handlers.
  • Heroku, Render, Fly.io — your app reads the $PORT env var and listens on it. The platform routes external 80/443 traffic to your $PORT.
  • AWS EC2 / DigitalOcean Droplets / your VPS — you bind to whichever ports you want, configure firewalls to allow them, set up SSL termination yourself.

For Bible Quest (Vercel), the hosting layer means “port” is mostly an internal-dev concept. You don’t configure ports for production.


Common gotchas

  • localhost:80 doesn’t usually need a port in the URL. http://localhost/ works because 80 is the default. http://localhost:80/ is the same.

  • Browsers REJECT non-standard ports for HTTPS by default. https://example.com:8443/ works but feels weird. Some browsers warn for unusual ports on HTTPS.

  • Some networks block non-standard ports. Corporate or school networks may only allow 80 and 443 outbound. Your dev server on port 3000 may be unreachable from a coffee shop.

  • PORT env var is the modern convention. Heroku started it; now Render, Fly, Railway all use it. process.env.PORT || 3000 is the idiomatic fallback.

  • Privileged ports (under 1024) require admin on most OSes. A Node app binding to port 80 needs sudo or setcap. Vercel/AWS/etc. handle this by running your code as a non-privileged user behind a load balancer that owns port 80.

  • Dev tools commonly fight over 3000. Many local databases, tools, and dev servers want port 3000. Use 3001+ if you have a habitual port conflict.

  • Multiple processes can’t bind the same port. EADDRINUSE means another process owns it. The OS won’t share.

  • 0.0.0.0 and 127.0.0.1 differ for binding. 0.0.0.0:3000 accepts connections from ANYWHERE on the network. 127.0.0.1:3000 only from this machine. Dev servers usually bind 127.0.0.1 for safety; production servers bind 0.0.0.0.

  • Firewalls can silently drop port traffic. A server listening on port 3000 + a firewall blocking 3000 = “can’t reach the server” with no error from the server’s perspective. Test from both directions.

  • Port forwarding on routers (NAT) — to expose a home server publicly, the router needs to forward an external port to your machine’s internal port. Otherwise the public can only reach the router, not the device behind it.

  • Tunneling services (ngrok, Cloudflare Tunnel) create a public URL that maps back to a local port. Saves dealing with NAT/firewalls during development. See Windows dev environment.

  • Some platforms restrict outbound ports. Vercel Functions can outbound to most ports, but some cloud providers restrict outbound 25 (SMTP) to prevent spam from compromised servers.

  • Wireshark / browser DevTools don’t show ports often. They focus on URLs. To see port-level traffic, use tcpdump, nslookup, or netstat.

  • http://localhost:3000/api is a connection to port 3000. The /api is just the URL path; the port determines which process answers.

  • WSL2 and Windows host share ports asymmetrically. A server inside WSL bound to 0.0.0.0:3000 is reachable from Windows host as localhost:3000. The reverse isn’t always true; WSL networking has its quirks.

  • Docker maps host ports to container ports. docker run -p 3000:3000 means “host port 3000 → container port 3000.” You can map differently: -p 3001:3000 makes the container’s 3000 reachable from host’s 3001.

  • docker-compose.yml uses ports: ["3000:3000"] for the same pattern. First number is host, second is container.

  • process.env.PORT is a STRING, not a number. parseInt(process.env.PORT) or use loosely (http.listen(process.env.PORT) accepts string).

  • The OS may refuse to release a port after a process crashes. TIME_WAIT state can hold a port for 30-60 seconds. Annoying when iterating during dev. kill -9 skips graceful shutdown but doesn’t immediately free the port; just wait.

  • Some Bonjour/mDNS services use port 5353. This is why “find devices on the network” works without manual IP entry — they announce on 5353.

  • Port scanning is detectable. Repeatedly trying random ports against a server is treated as reconnaissance and gets blocked / logged by firewalls. Don’t do it without permission.

  • A “service” running on a port can be ANYTHING. Port 80 doesn’t mean HTTP — it means “whatever’s listening here.” A misconfigured server could put a database on port 80. Always verify what’s actually listening.

  • netstat -an shows you all listening ports + connections on most OSes. ss -tulpn is the modern Linux equivalent. Useful for debugging “what’s running on this machine?”

  • telnet host port checks if a port is reachable. Open a TCP connection to host:port — if it connects, the port is open. Modern alternative: nc -zv host port. Quick test.

  • HTTPS on a non-443 port still works. Browsers do TLS to whatever port you ask. But certificate validation works the same way — the certificate must cover the hostname.

  • TLS termination usually happens at a load balancer. Behind a Vercel function or a Cloudflare worker, the request comes in on port 443 (HTTPS) and is passed to your function in plain HTTP — TLS is “terminated” at the edge. Your function never sees the encrypted bytes.

  • Reserved ports for testing: 1024-49151 ephemeral range avoids well-known. For local testing of custom services, pick something in 8000-9000 or 30000-40000 to avoid conflict.

  • PostgreSQL default is 5432, but Supabase pgbouncer is 6543. Direct connection vs pooled connection. Different ports for different purposes on the same logical service.


A practical reference

For Bible Quest specifically, the ports you’ll see:

  • Local dev: localhost:3000 (Next.js)
  • Local Supabase (if running locally): localhost:54321 (REST API), localhost:54322 (Postgres direct)
  • Production: Vercel handles 443 / HTTPS transparently. Supabase Postgres on 5432/6543 (you don’t expose this publicly)

Most of the time you just think localhost:3000. The rest is platform internals.


See also


Sources