Home › Database › PyMySQL 2003 Can't Connect — Network Fix
Beginner 6 min · September 23, 2026

PyMySQL 2003 Can't Connect — Network Fix

Fix PyMySQL error 2003: confirm MySQL listens, correct host/port, open firewalls, and use Docker service names.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 8 min
  • ✓A MySQL server you can start, stop, and inspect (local or Docker)
  • ✓Python 3 with PyMySQL installed (pip install pymysql)
  • ✓Basic networking: ports, firewalls, and DNS resolution
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Error 2003 means the TCP connection never completed — MySQL never saw your credentials, so don't touch grants or passwords yet
  • Confirm the server first: mysqladmin ping plus ss -ltn on the DB host proves mysqld is up and listening on 3306
  • Refused (errno 111) means the server side; timed out (errno 110) means firewall, security group, or wrong IP in between
  • In Docker, localhost is the app container itself — set the host to the Compose service name like db on a shared network
  • Error 1045 is a different failure past this one: only chase user@host grants after a raw TCP connect succeeds
✦ Definition~90s read
What is PyMySQL 2003 Cant Connect Fix?

PyMySQL error 2003 — pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ...") — means the client's TCP connection attempt to MySQL failed before any protocol exchange. No handshake ran, no credentials were sent, no grant was evaluated.

★
Think of error 2003 as calling a shop and never getting a ring — not a wrong password, not a locked door, just no answer.

The operating system refused or dropped the SYN: errno 111 (connection refused) when nothing listens on the target port, errno 110/113 (timed out / unreachable) when packets die in transit, or a DNS errno when the hostname doesn't resolve. It's the same 2003 the mysql CLI and every other connector report, because the failure sits below all of them in the TCP layer.

The five usual causes form a natural outside-in checklist: mysqld isn't running, it runs but doesn't listen on TCP (skip-networking, socket-only bind, or wrong port), the app dials the wrong host or port, a firewall or security group drops the packets, or — in containers — the hostname is a service name that doesn't resolve on the app's network. Docker's localhost trap deserves its fame: each container owns its loopback, so MYSQL_HOST=localhost in a containerized app dials the app itself and fails with 111 while the database sits healthy.

Error 2003's famous twin is 1045 (access denied), and the distinction organizes all debugging: 2003 is reachability, 1045 is identity. Everything about passwords, user@host grants, and auth plugins belongs to 1045 and is useless against 2003. Conversely, the moment a failing endpoint starts returning 1045 instead of 2003, the network is proven and the grants runbook takes over.

Teams that internalize this split resolve both errors in minutes; teams that mix them rotate passwords against a firewall.

Plain-English First

Think of error 2003 as calling a shop and never getting a ring — not a wrong password, not a locked door, just no answer. Either the shop is closed (MySQL isn't running), you dialed the wrong number (bad host or port), the road is blocked (firewall), or you asked for the shop by a nickname nobody recognizes (Docker name that doesn't resolve). Error 1045, by contrast, is someone picking up and saying your ID is invalid — a totally different problem past the point 2003 never reaches.

Your app boots, tries to open its database connection, and dies instantly: pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on 'db' ([Errno 111] Connection refused)"). No query ever ran. No credential was ever checked. The client never reached a server at all.

Error 2003 is purely a reachability failure — the TCP connection to MySQL's port never completed. That simplicity is deceptive, because the break can sit in any of five layers: MySQL isn't running, it's bound to the wrong interface, the host or port is wrong, a firewall eats the packets, or a container name doesn't resolve. Each layer produces the same 2003 with a slightly different errno, and guessing the layer wrong burns hours.

The stakes are practical: 2003 is the most common database error in fresh deploys, Docker moves, and cloud migrations. It always strikes at the worst moment — when you have the least diagnostic tooling and the most time pressure. A systematic outside-in check (server up, listening, reachable, resolvable, allowed) resolves it in minutes; random config edits can eat a day.

This guide gives you that order: confirming the server listens, fixing host and port, opening the network path, wiring Docker names correctly, and keeping 1045 grants work out of a 2003 problem. You'll also get timeouts and healthchecks that turn future 2003s into fast, obvious failures.

Read the 2003 Tuple: Host, Port, and Errno

PyMySQL raises OperationalError 2003 when socket.create_connection to the MySQL host and port fails. The message packs three facts: the hostname it tried, the errno from the OS, and sometimes the resolved IP. Errno 111 (connection refused) means SYN packets reached the host but nothing listens on that port. Errno 110 or 113 (timed out / no route) means packets died in transit — firewall, security group, or a wrong address. Errno -2 or -3 means DNS resolution itself failed, which is a naming problem, not a MySQL problem.

Read the full tuple before touching anything: (2003, "Can't connect to MySQL server on 'db' ([Errno 111] Connection refused)"). The quoted name is the exact host value your config supplied — compare it against the topology you intended. If it says localhost while you meant a remote host, the bug is in config loading, not networking. If it shows a stale IP, suspect cached DNS or an env file from another environment.

Crucially, 2003 happens before authentication. No password was sent, no grant was evaluated, no plugin negotiated. Any fix involving GRANT, ALTER USER, or password rotation cannot help a 2003 — those address 1045, a later failure. Logging the attempted host, port, and errno with every 2003 turns a mystery into a map: the numbers tell you which layer to inspect first.

PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pymysql

# Fail fast with diagnostics: short timeout, explicit host/port, full error context.
try:
    conn = pymysql.connect(
        host="db", port=3306, user="app",
        password="secret", database="shop",
        connect_timeout=5, read_timeout=10, write_timeout=10,
    )
except pymysql.err.OperationalError as exc:
    code, msg = exc.args[0], exc.args[1]
    print(f"mysql connect failed: code={code} msg={msg}")
    print("host=db port=3306 -- check listener, firewall, DNS in that order")
    raise
📊 Production Insight
Log the resolved IP alongside the hostname on every connection failure. Stale DNS is invisible when logs show only names — the IP reveals whether you even dialed the right machine.
🎯 Key Takeaway
Parse host, port, and errno from the message — refused means server-side, timeout means network, DNS errors mean naming.

Is MySQL Up and Listening on TCP?

A surprising share of 2003s mean MySQL simply isn't running — a failed upgrade, an OOM kill, a data-directory permission change after a restore. Check from the DB host itself: systemctl status mysql (or mysqld on RHEL-family) tells you whether the service is alive, and the error log — usually /var/log/mysql/error.log — tells you why it died. Look for InnoDB corruption lines, port-in-use lines, or permission denials on the data directory.

Next confirm the listener: ss -ltn | grep 3306 should show a LISTEN line. Its address matters — 127.0.0.1:3306 accepts loopback only, 0.0.0.0:3306 accepts all interfaces, and a missing line with a running server means skip-networking is set or the port is remapped. Match this against bind-address in my.cnf (grep -R bind-address /etc/mysql/) and the port setting, remembering that Docker -p mappings and managed-instance parameter groups can override file config.

Then prove the handshake without credentials: mysqladmin -h 127.0.0.1 -P 3306 ping expects mysqld is alive. If ping succeeds locally but the app fails remotely, the server is fine and the fault lies between hosts — firewall, routing, or wrong address. If ping fails locally too, stay on the server: fix the service before chasing networks.

Never restart blindly. A restart that clears a full disk or a deadlock works for a day and hides the cause; read the error log first so the fix addresses why it stopped.

BASH
1
2
3
4
5
6
7
8
# On the DB host: is MySQL alive, listening, and answering?
sudo systemctl status mysql --no-pager
mysqladmin -h 127.0.0.1 -P 3306 ping
ss -ltn | grep 3306
# Expect: LISTEN 0  151  0.0.0.0:3306  0.0.0.0:*

grep -R "bind-address\|skip-networking\|^port" /etc/mysql/ 2>/dev/null
sudo tail -n 50 /var/log/mysql/error.log
📊 Production Insight
OOM-killed MySQL is the classic recurring 2003: the service dies nightly under backup-plus-traffic load, restarts clean, and leaves no app-side trace except refused connections. dmesg plus memory graphs confirm it in minutes.
🎯 Key Takeaway
Prove mysqld is alive and listening on the expected interface before investigating anything past the server.

Wrong Host or Port: Test the Exact Endpoint

With the server proven, verify the address the app dials. Print the effective config — not the file you think it read, but the values after env substitution: grep -R MYSQL_HOST .env docker-compose.yml k8s/ and echo the DSN the app logs at startup. Then test that exact host and port from the app host with nc -zv db-host 3306, which performs the same TCP connect PyMySQL will attempt, minus credentials.

The localhost trap bites here. Most MySQL clients treat localhost as the Unix socket, but PyMySQL connects over TCP to 127.0.0.1 — so CLI success via localhost proves nothing about PyMySQL's path. Test both explicitly: mysql -h localhost for the socket path and mysql -h 127.0.0.1 --protocol=TCP for the TCP path. If the socket works but TCP fails, the server runs with skip-networking or binds the socket only.

Ports get remapped more often than teams expect: Docker -p 3307:3306, managed replicas on nonstandard ports, and SSH tunnels that forward a local port. Confirm the server's real port from my.cnf and from ss -ltn, then make the app's MYSQL_PORT match exactly. A connect to the wrong port on a live host returns refused fast — indistinguishable from a dead database unless you check.

Adopt one rule: the startup log prints the exact host, port, and connection type on every boot. When 2003 strikes, that line tells you what was dialed before you reproduce anything.

BASH
1
2
3
4
5
6
7
8
9
10
# From the APP host: test the exact endpoint the app will dial.
nc -zv db-host 3306
python3 -c "import socket; print(socket.create_connection(('db-host', 3306), timeout=5))"

# Socket vs TCP: prove which path works.
mysql -h localhost -u app -p -e 'SELECT 1'          # socket path
mysql -h 127.0.0.1 --protocol=TCP -u app -p -e 'SELECT 1'  # TCP path

# Show what the app really configured (after env substitution).
grep -RH "MYSQL_HOST\|MYSQL_PORT\|DB_HOST" .env docker-compose.yml
📊 Production Insight
SSH tunnels cause the weirdest host/port 2003s: the tunnel drops, autossh hasn't redialed, and the app's localhost:3307 suddenly refuses. Monitor the tunnel process itself, not just the app.
🎯 Key Takeaway
Test the exact host and port from the app host with nc — and remember PyMySQL's localhost means TCP, not the socket.

Firewalls and Security Groups: Prove the Split

When local connects succeed but the app host fails, packets are dying between machines — firewall, security group, or routing. Confirm the split cleanly: run nc -zv db-host 3306 on the DB host (expect success) and on the app host (observe timeout). Success-here plus timeout-there is the signature of a network policy drop, not a server fault.

Inspect each enforcement point in path order. Host firewalls: iptables -L -n or ufw status numbered on the DB host, looking for an INPUT rule on 3306. Cloud security groups: the DB instance's inbound rules must admit 3306 from the app's security group or subnet — a rule scoped to a stale CIDR after a VPC renumber is a classic silent killer. Corporate egress proxies and Kubernetes NetworkPolicies can also swallow 3306 while SSH sails through, so verify the port specifically, not general connectivity.

Fix narrowly and durably: open 3306 for the app subnet or reference the app security group by ID rather than CIDR, so future rescaling keeps working. Commit the rule to Terraform or the equivalent — a console click that fixes today's 2003 becomes next quarter's mystery outage when the environment is rebuilt without it. Then re-run nc from the app host until it succeeds before restarting the app.

Never answer a firewall 2003 by binding MySQL wider or disabling the firewall. Reachability should come from precise allow-rules, not from removing layers.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
# Prove the split: same test, two vantage points.
# On DB host (expect: succeeded):
nc -zv 127.0.0.1 3306
# On APP host (observe: timeout = policy drop):
nc -zv db-host 3306

# Inspect host firewall and listening scope on the DB host.
sudo iptables -L INPUT -n --line-numbers | grep 3306
sudo ufw status numbered
ss -ltn | grep 3306

# After fixing the rule, verify from the app host before restarting anything.
nc -zv db-host 3306 && echo REACHABLE
📊 Production Insight
VPC peering and PrivateLink moves cause the subtlest firewall 2003s: DNS resolves to a new private IP in a peered VPC whose route tables nobody updated. The security group looks right, but packets have no route home.
🎯 Key Takeaway
Local-success plus remote-timeout proves a policy drop — open 3306 narrowly for the app source and codify the rule.

Docker Service Names: localhost Is Yourself

Container networking gives localhost a new meaning, and 2003 is how teams learn it. Inside a container, localhost is that container — so an app configured with MYSQL_HOST=localhost dials itself on 3306 and gets refused by its own loopback. The database lives at its Compose service name (db), resolvable via Docker's embedded DNS only when both containers share a network.

Wire it correctly: set the app's DB host to the service name, keep both services on the same user-defined network (Compose does this by default per project), and expose the DB port to the app through the internal listener — no host port publishing required for container-to-container traffic. Verify from inside the app container: docker compose exec api getent hosts db should print the DB container's IP, and nc -zv db 3306 should succeed.

Then eliminate the boot race. depends_on alone only orders container start, not readiness — the app can still dial while MySQL initializes and log a 2003 that looks like a config bug. Add a healthcheck to the db service running mysqladmin ping and gate the app on service_healthy (Compose v2 syntax) or a wait-for-it entrypoint. Retries with backoff in the app's connect path cover the residual window.

Kubernetes rhymes: localhost is the pod, the DB host is a Service DNS name like mysql.prod.svc.cluster.local, and NetworkPolicies play the firewall role. Same outside-in checks, same resolution.

📊 Production Insight
The nastiest variant: it works with docker run --network host but fails under Compose, because host networking borrows the host loopback while bridged networking isolates it. That difference is the whole diagnosis.
🎯 Key Takeaway
In containers the DB host is the service name on a shared network — never localhost — plus a healthcheck so the app can't outrun the database.

1045 Is a Different Error: Switch Runbooks

Error 1045 — access denied for user@host — is the error you get after beating 2003, and confusing the two wastes entire incidents. 2003 means no TCP connection; no password was ever sent. 1045 means the connection succeeded and the server rejected the credentials, the host grant, or the auth plugin. Password rotations, GRANT statements, and plugin changes fix 1045 and cannot fix 2003.

Use 1045 as your proof of reachability. When mysql -h db-host -u app -p starts returning 1045 instead of 2003, the network path works — celebrate, then switch tools: SELECT user, host, plugin FROM mysql.user to find the account rows, SHOW GRANTS FOR 'app'@'app-host' to read its privileges, and ALTER USER to align the password and plugin. The host part matters because 'app'@'%' and 'app'@'10.0.0.5' are different accounts that match different source addresses.

Keep the runbooks separate. The 2003 runbook is network: listener, host/port, firewall, DNS, container names. The 1045 runbook is identity: user@host row exists, password matches, plugin compatible, privileges granted, FLUSH PRIVILEGES if edited directly. Running the wrong runbook is how a 20-minute network fix becomes a 3-hour credential rotation with the same error at the end.

One operational habit prevents the confusion: log the numeric error code with every database failure. Codes don't lie — 2003 sends you to the network, 1045 sends you to grants.

SQL
1
2
3
4
5
6
7
8
9
10
11
# 2003 defeated when this stops refusing and starts denying:
mysql -h db-host -P 3306 -u app -p -e 'SELECT 1'
# ERROR 1045 (28000): Access denied for user 'app'@'10.0.0.5' -- GOOD: network works.

-- Now (and only now) inspect identity:
SELECT user, host, plugin FROM mysql.user WHERE user = 'app';
SHOW GRANTS FOR 'app'@'10.0.0.5';

-- Typical fix: create the exact user@host row the app connects from.
-- CREATE USER 'app'@'10.0.0.5' IDENTIFIED BY 'secret';
-- GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app'@'10.0.0.5';
⚠ Never Rotate Passwords for a 2003
A 2003 means no connection existed, so no password was ever checked. Rotating secrets or rewriting GRANTs cannot help and burns the exact hour you need for the network path. Prove TCP with nc first; only touch credentials after the error becomes a 1045.
📊 Production Insight
Cloud IAM database auth adds a third act: short-lived tokens that expire mid-connection look like 1045s after a working 2003-free stretch. Check token TTL before re-diagnosing the network.
🎯 Key Takeaway
2003 is reachability, 1045 is identity — prove TCP with nc first, and only open the grants runbook after 2003 becomes 1045.
● Production incidentPOST-MORTEMseverity: high

Docker Migration Pointed the API at Itself for 90 Minutes

Symptom
Right after a Docker migration deploy, every API request failed with pymysql.err.OperationalError 2003, errno 111 connection refused. The database container showed healthy, CPU idle, no error-log entries — it was never receiving connections. Two rollbacks didn't help because all builds carried the same localhost value, stretching a config bug into 90 minutes of full outage.
Assumption
Everyone assumed the database was down. The deploy had bumped a dependency, so the team rolled back twice — each rollback taking 20 minutes — while 2003s continued. Then they assumed credentials, rotating the DB password and updating secrets, which changed nothing because no connection ever lived long enough to check a password. Two wrong theories cost an hour before anyone ran a packet-level check.
Root cause
The app read MYSQL_HOST=localhost from a legacy .env baked for host installs. On the old VM that meant the co-located database; inside the new app container it meant the container's own loopback, where nothing listens on 3306. The database container was healthy the entire incident — every 2003 was errno 111 refused by the app container itself. No rollback could fix it because the bad value shipped in all three builds.
Fix
The immediate fix was one line in the fog-of-war: MYSQL_HOST=db instead of localhost in the container env, and the API recovered in minutes. The durable fixes landed the same week: a mysqladmin-ping healthcheck with depends_on: condition: service_healthy, a startup probe that logs the resolved DB IP before connecting, connect_timeout=5 on every PyMySQL call, and a CI job that boots the Compose stack and asserts the API serves traffic — the exact scenario that had just failed.
Key lesson
  • localhost inside a container is the container itself — every Docker move must re-audit what the DB host means.
  • A 2003 never involves passwords — proving reachability with nc before touching secrets saves a wasted rollback.
  • Boot-order races need healthchecks, not sleeps — depends_on plus mysqladmin ping beats a hardcoded delay.
Production debug guideWork outside-in from the server to the socket — the first failing check names the broken layer.5 entries
Symptom · 01
Nothing answers on port 3306 at all
→
Fix
On the DB host run sudo systemctl status mysql (or mysqld) and mysqladmin -h 127.0.0.1 -P 3306 ping. Expect mysqld is alive. Then run ss -ltn | grep 3306 and confirm a LISTEN line. If nothing listens, check bind-address in my.cnf with grep -R bind-address /etc/mysql/ and the error log via sudo tail -n 50 /var/log/mysql/error.log for a failed start or a port conflict.
Symptom · 02
App config may name the wrong host or port
→
Fix
From the app host run nc -zv db-host 3306 (or python3 -c "import socket; socket.create_connection(('db-host', 3306), timeout=5)"). Expect a succeeded line. Compare against the app's real config: grep -R MYSQL_HOST .env docker-compose.yml and confirm the host and port match the server's actual address from ss -ltn. If localhost works but 127.0.0.1 fails, the server may listen on socket only — check for skip-networking in my.cnf.
Symptom · 03
Local connects work but the app host times out
→
Fix
Run the same nc -zv test from the DB host itself (expect success) and from the app host (observe timeout). A local-success plus remote-timeout split proves packets die in transit. Inspect sudo iptables -L -n | grep 3306 or the cloud security group for an inbound 3306 rule covering the app subnet, add it, then re-run nc from the app host until it succeeds.
Symptom · 04
Containerized app can't resolve the database hostname
→
Fix
Inside the app container run getent hosts db (expect the DB container's IP) and nc -zv db 3306. If the name doesn't resolve, run docker network ls and docker network inspect on the app's network to confirm both containers share it. Fix the app's host to the Compose service name, keep ports: ["3306:3306"] only where needed, and add a healthcheck running mysqladmin ping so the app can't race the database.
Symptom · 05
You suspect credentials but haven't proven reachability first
→
Fix
Run mysql -h db-host -P 3306 -u app -p -e 'SELECT 1' from the app host. If you now get ERROR 1045 instead of 2003, celebrate — the network works and you've crossed into grants territory. Check SELECT user, host FROM mysql.user and SHOW GRANTS FOR 'app'@'app-host', fix the grant, and never confuse the two errors again: 2003 is reachability, 1045 is permission.
PyMySQL 2003 Causes — Confirm Each Before You Fix
Root CauseHow to ConfirmFixPrevention
MySQL isn't running or isn't listening on TCPmysqladmin ping fails and ss -ltn shows nothing on 3306Start the service and verify the port listens before touching app codeSupervise mysqld with systemd and alert on failed mysqladmin ping
Wrong host or port in app confignc -zv host 3306 fails while it succeeds from the DB host itselfCorrect MYSQL_HOST and MYSQL_PORT from the actual deployment topologyValidate connectivity in CI with the same host/port the app will use
Firewall or security group blocking 3306TCP connects locally on the DB host but times out from the app hostOpen 3306 for the app subnet in the firewall or security groupCodify firewall rules in Terraform and review them with the schema
Docker/Compose service-name resolution failuregetent hosts db fails inside the app container but ping works by IPUse the Compose service name as host and put both services on one networkAdd depends_on plus a mysqladmin-ping healthcheck to order startup
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
try:Read the 2003 Tuple
sudo systemctl status mysql --no-pagerIs MySQL Up and Listening on TCP?
nc -zv db-host 3306Wrong Host or Port
nc -zv 127.0.0.1 3306Firewalls and Security Groups
mysql -h db-host -P 3306 -u app -p -e 'SELECT 1'1045 Is a Different Error

Key takeaways

1
2003 means unreachable
refused points at the server, timeout points at the network path between you and it.
2
Check outside-in
server running, port listening, host/port correct, name resolves, firewall allows.
3
localhost is a socket in most clients but TCP in PyMySQL
know which path you're actually testing.
4
In Docker, the DB host is the Compose service name, never localhost
plus a healthcheck to order startup.
5
1045 is a different error
never rotate passwords or rewrite grants to fix a 2003.
6
Fail fast with connect_timeout and log host, port, and errno so the next 2003 explains itself.

Common mistakes to avoid

5 patterns
×

Using localhost when you meant TCP (or vice versa)

Symptom
Connect works from the mysql CLI but fails from PyMySQL with 2003, or works in Docker but fails on the host — the two clients take different paths to the same server.
Fix
Use 127.0.0.1 for TCP loopback or keep localhost only when you intend the socket. Better still, make host selection explicit per environment and assert the effective connection type in a startup log line.
×

Pointing a containerized app at localhost for the database

Symptom
The app container throws 2003 on every boot while the database container is healthy — localhost inside the container is the app itself, not the DB.
Fix
Set MYSQL_HOST to the Compose service name (db), attach both services to the same network, and gate the app on a mysqladmin ping healthcheck so it can't race the database.
×

Treating 2003 like 1045 and rotating passwords

Symptom
Hours lost resetting passwords and reissuing GRANTs while the server was never reachable — the error never changed because credentials were never the problem.
Fix
Read the error number first: 2003 means unreachable, 1045 means wrong credentials. For 1045 check the user@host grant and password; for 2003 check the network. Never rotate passwords to fix a 2003.
×

Opening no firewall rule — or opening 3306 to the world

Symptom
Local connects succeed but the deployed app times out with 2003, or a scan report flags a world-open database port that a rushed hotfix created.
Fix
Open 3306 narrowly for the app subnet or a security-group reference, verify with nc from the app host, and commit the rule to infrastructure code so the next environment rebuild keeps it.
×

Connecting with no timeout and no diagnostics

Symptom
A dead database hangs the app for minutes per request instead of failing fast, and the logs show a bare 2003 with no hint which host, port, or stage failed.
Fix
Pass connect_timeout=5 (and read/write timeouts) in every PyMySQL connect call so failures surface in seconds. Log the resolved host, port, and elapsed time with each connection error.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What's the difference between MySQL errors 2003 and 1045?
Q02JUNIOR
Connection refused versus connection timed out — what does each tell you...
Q03SENIOR
Walk me through diagnosing a 2003 from the app server.
Q04SENIOR
Why does localhost break when the app moves into Docker?
Q05SENIOR
How do you make a service resilient to 2003s at deploy time and runtime?
Q01 of 05JUNIOR

What's the difference between MySQL errors 2003 and 1045?

ANSWER
2003 means the TCP connection never completed — refused or timed out before any authentication. 1045 means the connection succeeded but the server rejected the user@host and password combination. For 2003 I check the network path (server up, listening, host/port, firewall); for 1045 I check grants, passwords, and auth plugins.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How is error 2003 different from error 1045?
02
Can I use telnet to test MySQL connectivity?
03
Does PyMySQL use the Unix socket or TCP?
04
What does refused versus timed out tell me?
05
Is bind-address 0.0.0.0 safe for production?
06
Should I always set a connect timeout in PyMySQL?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Everything here is grounded in real deployments.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's MySQL. Mark it forged?

6 min read · try the examples if you haven't

←
Previous
MySQL 1364 No Default Value Fix
7 / 7 · MySQL