Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Port 3306: What MySQL Listens On, and Why It Should Not Be Open to the Internet

Sean

Platform Writer

Aug 17, 2026
9 min read

3306/TCP is the default port for the classic MySQL protocol — the one mysql, MySQL Workbench, and every language connector use. It is not the only MySQL port: 33060 carries the X Protocol and 33061 handles Group Replication internals, which is why a port scan of a MySQL host shows more than you expected.

Port 3306: What MySQL Listens On, and Why It Should Not Be Open to the Internet

Most searches that land on this number are one of three problems: something else has taken the port, MySQL is running but refusing connections, or somebody is deciding whether to open it in a firewall. The first two have short answers. The third has a short answer too, and it is no.

Table of contents

The port map around MySQL

  • 3306/TCP — classic MySQL protocol. The default for mysql, connectors, Workbench, and replication between a replica and its source.
  • 33060/TCP — X Protocol, used by MySQL Shell in X mode and the document store API. Derived from the classic port with a zero appended, which is a nice piece of design and a surprise when it appears in a scan.
  • 33061/TCP — Group Replication internal communication between cluster members. Not a client port.
  • Unix socket — on Linux, a local client connecting to localhost often uses /var/run/mysqld/mysqld.sock rather than TCP at all.

That last one causes a specific confusion worth naming. On MySQL, -h localhost means use the socket and -h 127.0.0.1 means use TCP. They are different transports with different authentication paths, so a connection that works one way and fails the other is not a contradiction. When testing whether the port is actually listening, always use 127.0.0.1.

mysql -h 127.0.0.1 -P 3306 -u appuser -p
mysql -h localhost -u appuser -p     # may use the socket instead

Checking whether anything is listening

sudo ss -tlnp | grep 3306
sudo lsof -i :3306
sudo netstat -tlnp | grep 3306

Read the address column, not just the presence of a line. 127.0.0.1:3306 means MySQL accepts local connections only. 0.0.0.0:3306 means it accepts from any interface. *:3306 is the same thing. The difference between those two is the entire remote-connection question.

From another machine, test reachability before blaming credentials:

nc -zv db.example.com 3306
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/db.example.com/3306' && echo open

telnet works too if it is installed. What matters is separating the network question from the authentication question — they produce different errors and people frequently debug the wrong one for twenty minutes.

Connection refused, and the three causes

ERROR 2003 (HY000): Can't connect to MySQL server on ... (111) means nothing accepted the TCP connection. In order of likelihood:

  1. MySQL is not running. systemctl status mysql or systemctl status mysqld depending on the distribution.
  2. It is bound to loopback only. bind-address = 127.0.0.1 in the config. This is the default on most packages and is a good default.
  3. A firewall is dropping it. ufw, firewalld, iptables, or a cloud security group.

For the second, the setting lives in /etc/mysql/mysql.conf.d/mysqld.cnf on Debian and Ubuntu, or /etc/my.cnf on Red Hat family systems:

[mysqld]
bind-address = 0.0.0.0
# or better, one specific private interface:
bind-address = 10.0.1.15

Restart after changing it. And read the next section before setting it to 0.0.0.0 on anything with a public IP.

A fourth cause that looks identical from the client: the user exists but is scoped to the wrong host. MySQL grants are per user-and-host pair, so appuser@localhost and [email protected].% are different accounts. A connection that authenticates from the database server and fails from the application server is usually this, not a network problem.

SELECT user, host FROM mysql.user;
CREATE USER 'appuser'@'10.0.1.%' IDENTIFIED BY 'strong-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'10.0.1.%';
FLUSH PRIVILEGES;

Address already in use

The other direction: MySQL will not start because something already holds 3306.

sudo lsof -i :3306
sudo ss -tlnp | grep 3306

The usual culprits, roughly in order:

  • An old MySQL or MariaDB instance still running from a previous install. Stop it properly rather than killing the process, so it flushes cleanly.
  • A Docker container publishing 3306 to the host. docker ps and look at the ports column.
  • A local development stack — XAMPP, MAMP, Laragon — that started MySQL at boot.
  • A previous instance that crashed and left the port held briefly. Wait, then retry.

If you genuinely need two instances, change the port rather than fighting over one. Set port = 3307 in the second instance’s config and pass -P 3307 when connecting. In Docker, map it on the host side and leave the container internal: -p 3307:3306.

Why 3306 should not be open to the internet

This is the opinion, and it is not a controversial one. A MySQL port reachable from the public internet is scanned continuously, and the entire security model reduces to one password.

What that exposes:

  • Credential guessing against a service whose failed-login handling is not designed as a public-facing defence.
  • Version disclosure in the handshake before authentication, which tells an attacker exactly which CVEs to try.
  • Any authentication bypass in your MySQL version becoming immediately exploitable rather than requiring a foothold first.
  • Unencrypted traffic, if TLS is not enforced — and enforcing it is a separate step people forget.

The alternatives are all straightforward:

  • Private networking. The database listens on an internal interface only; the application reaches it over a network the internet cannot route to. This is the right answer and should be the default.
  • SSH tunnel for occasional admin access: ssh -L 3307:localhost:3306 user@dbhost, then connect to 127.0.0.1:3307 locally. Nothing is exposed.
  • Allowlisted source addresses if remote access is genuinely required. Better than open, worse than private networking.
  • Enforced TLS with REQUIRE SSL on the account, in every case where traffic leaves a host.

The SSH tunnel deserves special mention because it covers the case people usually open the port for — connecting a GUI client from a laptop. One command, no firewall change, nothing left open afterwards.

Connection limits, which is the next thing to go wrong

Once the port is right, the failure moves up a layer. ERROR 1040: Too many connections means max_connections is reached, and the cause is almost always application-side pooling rather than genuine load.

SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
SHOW PROCESSLIST;

Compare Max_used_connections against max_connections. If they are close, the arithmetic to check is: number of application instances multiplied by pool size per instance, plus background workers, plus your own admin sessions. Autoscaling makes this worse quietly — every new instance brings its own pool, so scaling out the web tier can exhaust the database while each individual instance looks healthy.

Raising max_connections is the obvious move and often the wrong one, since each connection costs memory. Reducing per-instance pool size, or putting a connection pooler in front, usually gives a better result than letting the database hold thousands of mostly-idle threads.

How this fits the rest of the stack

Nearly every problem above is a consequence of running the database on a machine you also have to network, firewall, and patch. A managed instance moves the boundary: it is reachable from your services over private networking and not from the internet, connection limits are a documented number rather than a discovery, and backups are configuration rather than a cron job someone wrote in 2021. RunxBuild offers managed MySQL and Postgres on that model — databases on RunxBuild covers connecting, users, and limits, and database network security on RunxBuild covers the private networking side. When you are sizing one against a self-run instance, the RunxBuild hosting calculator shows the database, the service that talks to it, storage, and bandwidth as separate figures.

Useful related references:

FAQ

What is port 3306 used for?

It is the default TCP port for the classic MySQL protocol, used by the mysql client, MySQL Workbench, language connectors, and replication traffic between a replica and its source. MySQL also uses 33060 for the X Protocol and 33061 for Group Replication internals.

Why does MySQL refuse connections on port 3306?

Three usual causes: the server is not running, it is bound to 127.0.0.1 only via bind-address, or a firewall is dropping the packets. Check with sudo ss -tlnp | grep 3306 and read the address — 127.0.0.1:3306 means local connections only.

Should I open port 3306 to the internet?

No. A publicly reachable database port is scanned constantly and reduces your security to a single password, while disclosing the server version before authentication. Use private networking between your application and database, or an SSH tunnel for occasional admin access.

How do I fix address already in use on port 3306?

Find the holder with sudo lsof -i :3306. It is usually an old MySQL or MariaDB instance, a Docker container publishing the port, or a local dev stack like XAMPP. Stop it cleanly, or run the second instance on 3307 instead of contesting the port.

Why does localhost work but 127.0.0.1 fail in MySQL?

On Linux, -h localhost makes the client use a Unix socket while -h 127.0.0.1 uses TCP. They are different transports and MySQL grants are per user-and-host pair, so an account can be valid for one and not the other. Use 127.0.0.1 when testing whether the port itself is listening.

#3306 port#mysql port#mysql#database networking#bind-address