3306
The port a MySQL, MariaDB or Percona server listens on for the classic client protocol — and the one your driver reports it cannot reach.
Run the line for your platform. Each one names the process holding the port and the address it bound to, which is usually the whole answer.
sudo lsof -nP -iTCP:3306 -sTCP:LISTEN`-n` stops lsof resolving addresses to host names and `-P` stops it turning 3306 into a service name, so the NAME column stays numeric and readable; `-iTCP:3306` selects that port and `-sTCP:LISTEN` keeps only listening sockets. You get one row per socket: COMMAND is the program, PID is the number you would act on, USER is who started it, and NAME is the bind address. `127.0.0.1:3306` means loopback only — this machine can connect and no other ever will — while `*:3306` means every interface. No output at all (lsof exits 1) means nothing is listening, so a refused connection is honest rather than blocked. Without `sudo` you see only your own processes, so a server running as `_mysql` or inside Docker looks invisible; a COMMAND of `com.docker.backend` means a published container port, so stop the container instead of killing that PID.
sudo ss -tlnp 'sport = :3306'`-t` displays TCP sockets, `-l` displays only listening sockets, `-n` stops service-name resolution and `-p` shows the processes using the sockets; `sport = :3306` is ss's filter predicate for the source port. Read the Local Address:Port column first: `0.0.0.0:3306` is every IPv4 interface, `127.0.0.1:3306` is loopback only and explains a remote client that never connects, and `[::]:3306` is the IPv6 wildcard, which on most Linux hosts accepts IPv4 as well. The owner appears in the last column as `users:(("mysqld",pid=1234,fd=25))` — the name in quotes, then the PID. On a host with no `ss`, the net-tools spelling is `sudo netstat -tlnp | grep :3306`, where `-p` prints a slash-separated pid/program pair; both need superuser privileges to show processes you do not own, and without them the process column is simply blank while the socket still lists.
netstat -ano | findstr ":3306"`-a` displays all connections and the ports the computer is listening on, `-n` expresses addresses and ports numerically, and `-o` includes the process ID for each connection — the PID is the last column. Local Address is the bind: `127.0.0.1:3306` is loopback only, `0.0.0.0:3306` is every interface, and the State column must read LISTENING for the row to be the listener rather than a client connection. Turn the PID into a program with `tasklist /FI "PID eq 1234"`, whose PID filter accepts eq, ne, gt, lt, ge and le. Watch the match: `findstr` compares substrings, so `:3306` also matches `:33060`, MySQL's X Protocol port — read the number in full before you kill anything.
Get-NetTCPConnection -LocalPort 3306 | Select-Object LocalAddress,LocalPort,State,OwningProcessThe PowerShell equivalent, and it matches the port exactly rather than by substring, which is what makes it worth typing next to `netstat`. `State` of `Listen` with a `LocalAddress` of `127.0.0.1` is the exact configuration that answers locally and refuses everyone else. Pipe the `OwningProcess` value into `Get-Process -Id` for the executable name. An empty result means nothing holds the port at all — if a bind still fails at that point the port is reserved rather than occupied, and `netsh interface ipv4 show excludedportrange protocol=tcp` lists the blocks Hyper-V, WSL2 and Docker Desktop claim at boot.
`nmap -Pn -p 3306 db.example.com` asks the only question that matters from outside the host, and `-Pn` skips host discovery so a machine that ignores pings still gets scanned. Nmap's own definitions decide what you learn: `open` means an application on the target is listening on that port; `closed` means no application is listening, though the host answered, so it is reachable and something merely is not running; `filtered` means a firewall, filter or other network obstacle is blocking the port so nmap cannot tell whether it is open or closed. Add `--reason` to see the packet that decided it — a RST from a closed port versus nothing at all from a filtered one — and `-sV` to let nmap read the MySQL handshake and report the server version. Scan only hosts you are responsible for.
| What you see | What it means |
|---|---|
ERROR 2003 (HY000): Can't connect to MySQL server on 'db.example.com:3306' (111) | Errno 111 is `ECONNREFUSED` on Linux, 61 on macOS: the packet reached the host and the kernel replied with a reset because no process holds the port. The server is stopped, crashed during startup, or bound to an address that does not include the one you dialled. Run the probe for your platform on the database host — not on the client — and read the bind address rather than just confirming a row exists. |
The client hangs for tens of seconds, then reports (110) or a connect timeout | Errno 110 is `ETIMEDOUT`: nothing answered at all. A packet filter is dropping the SYN silently — a cloud security group, an on-host firewall, or a network ACL between you and the database. A refusal is instant; a drop is slow, and the delay itself is the diagnosis. Confirm from outside with `nmap --reason`: `filtered` is the drop, `closed` is the refusal. |
[ERROR] Do you already have another mysqld server running on port: 3306 ? / bind: Address already in use | `EADDRINUSE`: something already holds the socket, and mysqld exits during startup rather than after. Nine times out of ten it is a second MySQL or MariaDB from a different install path, or a container publishing 3306 on the host. Name the owner with the probe above before killing anything, and if the port frees itself a minute or two later, the previous server left sockets in `TIME_WAIT` and nothing needed killing at all. |
bind: Permission denied while starting the server on 3306 | Not the privileged-port rule — that only covers ports below 1024, and 3306 is well above it. Look instead at a mandatory access control policy (SELinux or AppArmor) refusing the bind for that binary, at a container running without the capability, or at the data directory's ownership stopping the server before it ever reaches the socket. On Windows the same shape appears when the port sits inside a reserved exclusion range: nothing is listening, nothing can bind, and there is no PID to blame. |
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' | Port 3306 was never involved. You passed `localhost`, so the client used a Unix domain socket, and the path it tried does not exist — usually because the server is in a container, or a different package puts its socket somewhere else. Use `127.0.0.1` to force TCP, or point `--socket` at the real path. |
ERROR 1130 (HY000): Host '203.0.113.9' is not allowed to connect to this MySQL server | Good news: the port, the network and the server are all fine. The connection got far enough for MySQL to look up the account and refuse it. MySQL accounts are `user@host` pairs, so a grant for `app@localhost` rejects the same user arriving over TCP. Nothing in the network needs changing. |
ERROR 1045 (28000): Access denied for user 'app'@'10.0.0.5' (using password: YES) | Also not a port problem — the handshake completed and authentication was attempted and rejected. Wrong password, or the account uses an authentication plugin the client cannot handle, which is the usual reason an old driver fails against `caching_sha2_password` on MySQL 8. |
connect ECONNREFUSED 127.0.0.1:3306 from inside a container | Inside a container, `127.0.0.1` is that container — not the host and not the database container. Address the database by its Compose service name, or publish the port and use the host's address. The port number is right; the destination is not. |
IANA assigns 3306 to `mysql` for both TCP and UDP, but only the TCP half is ever used: it carries the classic MySQL client/server protocol that every driver speaks, from JDBC and `mysqlclient` to `mysql2` and Go's `go-sql-driver`. MariaDB and Percona Server inherit the same default, so one machine can run only one of them on 3306 at a time — which is why a database container refuses to start next to a Homebrew, APT or XAMPP install that is already up. MySQL's newer X Protocol lives on a different number, 33060, registered separately as `mysqlx`, so a socket line ending in 33060 is not the connection you are chasing. One detail catches almost everybody: with the official client libraries the host name `localhost` does not mean 127.0.0.1. It selects a Unix domain socket and the port is ignored entirely, so adding `-P 3306` to a `localhost` connection changes nothing at all. Write `127.0.0.1`, or pass `--protocol=TCP`, when the thing you actually want to test is the port. And ask the running server what it thinks it is doing — `SHOW VARIABLES LIKE 'port'` and `SHOW VARIABLES LIKE 'bind_address'` — rather than reading a config file it may never have loaded.
Do not expose 3306 to the internet. Anything that reaches it can attempt authentication as often as it likes, and public scanners find a fresh listener within hours. TLS is not the safety net people assume: the client default `--ssl-mode=PREFERRED` encrypts when the server offers it and continues in plaintext when it does not, and it verifies nothing, so it cannot detect an interceptor. Bind the server to a loopback or private address and check what it actually did with `SHOW VARIABLES LIKE 'bind_address'` — the upstream default is `*`, meaning every interface, while several distribution packages override it in their own config, so neither answer is safe to assume without asking. Reach a remote database through an SSH tunnel, a VPN or a private subnet. If it genuinely must be routable, restrict it by source address at the firewall, require TLS on the accounts rather than hoping for it, and remember that a grant of `user@'%'` accepts the whole internet by definition.
All of these run in your browser — nothing is uploaded.
Ports people usually end up checking in the same session as 3306.
Almost always permissions. lsof and ss list the socket but hide the owning process when it belongs to another user, so a server started by a system account, by a container runtime or by launchd shows up as an anonymous listener until you re-run the command with sudo. On Windows the opposite happens: netstat shows nothing at all, yet a bind still fails, which means the port sits inside a range that Hyper-V, WSL2 or Docker Desktop reserved at boot.
Look at what it is first. An orphaned dev server is safe to kill; a database with a write in flight is not, and neither is a system service that something else depends on. If the owner is a container, stop the container instead — killing the process the host sees only makes the runtime restart it. When the process is something you need, moving your own service to another port is the faster fix.
The listener bound to 127.0.0.1 rather than 0.0.0.0. The probes on every page print the bound address next to the process for exactly this reason: a loopback bind is reachable from the machine itself and from nowhere else, no matter how the firewall is configured. Many frameworks made that the default deliberately, so it is usually a flag rather than a bug. If the address is already 0.0.0.0 and it still fails, the next suspect is a host firewall or a cloud security group.
Barely. Internet-wide scanners sweep the whole port range continuously, so an unusual number buys hours, not safety, and it costs you every colleague who now has to remember it. It does cut the noise in your logs from opportunistic scans of the default port, which is a real if modest benefit. Authentication, a firewall that restricts source addresses, and not exposing the service at all are the measures that actually change the outcome.
Still stuck on port 3306? browse every port in the reference — or go back to the failure message you actually got, above.