Home › Database › MySQL 1045 Access Denied — Fix User, Host, Plugin
Beginner 5 min · September 23, 2026

MySQL 1045 Access Denied — Fix User, Host, Plugin

Fix MySQL error 1045 by matching the exact 'user'@'host' row, resetting the password, and aligning the auth plugin correctly..

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 11 min
  • ✓A MySQL 8.0 server you can query with a privileged account
  • ✓Basic comfort running mysql CLI commands and reading error logs
  • ✓Access to your app's connection string or secret-store entry
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • MySQL treats 'app'@'10.0.0.5' and 'app'@'%' as different accounts, so error 1045 usually means no row matches your exact user-plus-host pair
  • Check the plugin column: an account using caching_sha2_password can't log in through old clients that only speak mysql_native_password
  • After manual grant-table edits, run FLUSH PRIVILEGES so the server reloads privileges before you retry the login
  • Create users with CREATE USER 'app'@'10.0.0.5' IDENTIFIED WITH mysql_native_password BY 'secret', then GRANT only what's needed
✦ Definition~90s read
What is MySQL 1045 Access Denied Fix?

Error 1045 is MySQL's authentication rejection: the server looked at who you claim to be, where you're connecting from, and what credential you offered, and refused. The three inputs are the username, the host you connect from as the server sees it, and the password or plugin handshake.

★
Think of MySQL as an office building with a strict front desk.

The parenthetical at the end is a genuine clue: (using password: YES) means your client sent a password and it didn't match; (using password: NO) means it sent none at all, which points at a missing env var or an empty DSN field rather than a wrong secret.

The part most engineers miss is that MySQL accounts are pairs. The grant tables in mysql.user store User and Host columns together, and 'shop'@'localhost', 'shop'@'10.0.4.18', and 'shop'@'%' are three separate accounts with separate passwords and separate plugins.

When you connect, the server sorts all matching rows by specificity and picks the most specific one — so a leftover anonymous account like ''@'10.0.4.%' can shadow the account you intended. That's why SHOW GRANTS FOR 'shop'@'localhost' can look perfect while the app, connecting over TCP from another box, still gets 1045.

The second classic trigger is the authentication plugin. Since MySQL 8.0 the default is caching_sha2_password, but older connectors — PHP's mysqlnd before 7.4, old MySQLdb builds, some Java drivers — only speak mysql_native_password. The handshake then fails before the password is even compared.

One SELECT on mysql.user showing the plugin column tells you in seconds which world you're in, and ALTER USER ... IDENTIFIED WITH aligns it.

Plain-English First

Think of MySQL as an office building with a strict front desk. Your badge must show the right name and the right entrance — 'app' from gate '%' is a different visitor than 'app' from gate '10.0.0.5'. Error 1045 means the desk couldn't match your name-plus-gate combo or your password. There's a third check too: the language you speak. New servers speak caching_sha2_password while older apps only speak mysql_native_password, so you're turned away even with the right badge.

ERROR 1045 (28000): Access denied for user 'shop'@'10.0.4.18' (using password: YES). If you've run a MySQL-backed service for any length of time, you've stared at this line at the worst possible moment — right after a deploy, a password rotation, or a replica promotion. The database is up. The network is fine. Yet your app can't get in, and every retry burns another failed-login entry in the log.

The frustration is that 1045 is a bouncer, not a diagnostician. It won't tell you whether the username is wrong, the host part doesn't match, the password changed, or the authentication plugin disagrees with your client library. You have to interrogate each of those yourself, in the right order.

This guide gives you that order. You'll learn how MySQL matches 'user'@'host' rows, how to spot a caching_sha2_password versus mysql_native_password mismatch in one query, when FLUSH PRIVILEGES matters, and the exact GRANT syntax that creates least-privilege accounts. By the end, a 1045 will take you five minutes instead of fifty.

Read the Error Line Like a Log, Not a Complaint

Every 1045 line carries three facts if you slow down and parse it. The quoted pair 'shop'@'10.0.4.18' is the identity the server evaluated — username first, then the host as the server resolved it. The error code (28000) is the SQLSTATE for invalid authorization, useful when grepping logs across mixed database fleets. And the parenthetical is the password hint: (using password: YES) means a credential was offered and rejected, while (using password: NO) means the client sent nothing, which almost always means an empty secret, an unset env var, or a DSN that dropped the password field.

Your first two queries should come from a privileged account and should answer: which rows exist for this user, and which identity does a real connection actually match? SELECT user, host, plugin FROM mysql.user WHERE user='shop' lists every candidate row plus the plugin each one speaks. SELECT USER(), CURRENT_USER() shows the requested identity versus the row the server matched — when those differ, a shadowing row (often anonymous) is eating your login. Run both before you change anything, because guessing between wrong-password and wrong-row wastes the most time in these incidents. Keep both outputs in the ticket — the row list and the identity pair — since every escalation starts by asking for exactly those two.

diagnose_1045.sqlSQL
1
2
3
4
5
6
7
8
-- Which account rows exist for this user (host + plugin matter)
SELECT user, host, plugin FROM mysql.user WHERE user = 'shop';

-- Requested identity vs the row the server actually matched
SELECT USER(), CURRENT_USER();

-- Grants for the EXACT row your app matches (host part required)
SHOW GRANTS FOR 'shop'@'10.0.4.18';
📊 Production Insight
A team spent 40 minutes resetting a password that was correct — the app matched ''@'10.0.4.%' (anonymous) instead of their account. One SELECT USER(), CURRENT_USER() would have shown the mismatch in ten seconds.
🎯 Key Takeaway
Parse all three facts in the 1045 line (user, host, password YES/NO), then run the two diagnostic SELECTs before changing anything.

MySQL Users Are user@host Pairs, Not Bare Names

This is the mental model that fixes most 1045s permanently. MySQL never authenticates a bare 'shop' — it authenticates 'shop' coming from somewhere. 'shop'@'localhost' (socket connections), 'shop'@'10.0.4.18' (TCP from that IP), and 'shop'@'%' (any TCP host) are fully independent accounts: each has its own password hash, its own plugin, and its own grants. Creating one does nothing for the others, and rotating one leaves the others stale.

Matching follows specificity: the server sorts candidate rows and picks the most specific Host value that fits your source. That rule has two sharp edges. First, '%' does not cover socket connections — an app connecting via the Unix socket needs a 'localhost' row even if a '%' row exists. Second, leftover anonymous rows like ''@'localhost' (shipped by old installers) match broadly and get picked over your account, producing a 1045 that survives every password reset.

The fix is explicit provisioning: one CREATE USER per source your fleet uses, each with least-privilege GRANTs, and a cleanup pass that drops anonymous rows. When hosts are elastic, prefer a subnet pattern like 'shop'@'10.0.4.%' over '%' so a compromised box elsewhere can't reuse the credential.

create_user_host.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
-- One account per source your fleet actually uses
CREATE USER 'shop'@'10.0.4.18' IDENTIFIED WITH mysql_native_password BY 'change-me-strong';
CREATE USER 'shop'@'10.0.4.%' IDENTIFIED WITH mysql_native_password BY 'change-me-strong';

-- Least privilege: only what checkout needs, only on its schema
GRANT SELECT, INSERT, UPDATE ON shop.* TO 'shop'@'10.0.4.18';
GRANT SELECT, INSERT, UPDATE ON shop.* TO 'shop'@'10.0.4.%';
FLUSH PRIVILEGES;

-- Remove anonymous rows that shadow real accounts
SELECT user, host FROM mysql.user WHERE user = '';
-- DROP USER ''@'localhost';
⚠ '%' Doesn't Mean Everyone
'user'@'%' covers TCP connections only — never the Unix socket. If your app connects via localhost socket, you still need a 'localhost' row or you'll chase a phantom 1045.
📊 Production Insight
After an autoscaling recycle moved the fleet from .19 to .18, a rotation script kept updating the dead row. Explicit per-host rows plus a SELECT user, host audit in the runbook ended the whole class of incident.
🎯 Key Takeaway
Provision one account row per connection source, prefer subnet patterns over '%', and delete anonymous rows that steal matches.

Auth Plugin Mismatch: caching_sha2_password vs mysql_native_password

Since MySQL 8.0, new accounts default to caching_sha2_password, a stronger challenge-response handshake. The catch is purely client-side: connectors that predate support — old PHP mysqlnd, ancient MySQLdb, pinned Java drivers — can't complete that handshake, so the login dies with 1045 even though the password is right. You'll see this exactly once per stack: right after a server upgrade or when a fresh account meets a legacy app.

Diagnosis is one column: SELECT user, host, plugin FROM mysql.user WHERE user='shop' shows which handshake each row demands. Then check the client: mysql --version, the driver version in your lockfile, and a direct CLI login attempt. If the CLI from a modern client succeeds while the app fails, the app's connector is the problem, not the credential.

You have two honest fixes. The fast one is aligning the row to the client with ALTER USER ... IDENTIFIED WITH mysql_native_password BY 'secret'. The durable one is upgrading the connector so you can keep the stronger plugin. What you shouldn't do is downgrade every account server-wide because one legacy service complained — scope the exception, ticket the upgrade, and keep the default strong everywhere else.

fix_plugin_1045.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- Which handshake does each row demand?
SELECT user, host, plugin FROM mysql.user WHERE user = 'shop';
SHOW VARIABLES LIKE 'default_authentication_plugin';

-- Align one legacy row to its old client (scoped exception)
ALTER USER 'shop'@'10.0.4.18' IDENTIFIED WITH mysql_native_password BY 'actual-secret';
FLUSH PRIVILEGES;

-- Verify from a host using TCP, like the app does
-- (run in shell) mysql -u shop -p -h db-primary -e 'SELECT CURRENT_USER();'
📊 Production Insight
A PHP 7.2 checkout service broke the night the cluster moved to MySQL 8: CLI logins worked, app logins 1045'd. The plugin column named the culprit in one query; a scoped ALTER bought time while the driver upgrade was scheduled.
🎯 Key Takeaway
When the password is right but 1045 persists after an upgrade, compare the row's plugin against what your connector supports — then align or upgrade.

FLUSH PRIVILEGES and GRANT Syntax That Actually Works

FLUSH PRIVILEGES tells a running server to reload the grant tables into memory. You need it after any direct write to mysql.user (UPDATE, INSERT) and on older versions after CREATE USER performed oddly through automation — but modern GRANT, CREATE USER, and ALTER USER statements reload automatically, so a missing flush is rarely the cause on MySQL 8 unless something touched the tables by hand. Knowing this saves you from cargo-culting FLUSH after every statement while still running it where it counts.

GRANT syntax itself causes a quieter class of 1045-adjacent pain: privileges granted to the wrong host row. GRANT SELECT ON shop. TO 'shop'@'10.0.4.18' affects only that row; the fleet's other rows stay unprivileged. Always include the host part, always name the schema (never .* for app accounts), and always verify with SHOW GRANTS FOR the exact row. The verification query is the deploy gate: if SHOW GRANTS doesn't list what the app needs on the row the app matches, the login may succeed but the first query dies — which pages you just as loudly at 2 AM.

For rotation runbooks, make the sequence atomic from the app's view: ALTER the password on every host row, FLUSH once, verify SELECT 1 from an app host, then close the ticket. Partial rotations across rows are how 'it works from staging but not prod' is born.

grants_1045.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
-- Least-privilege grant pinned to the exact row
GRANT SELECT, INSERT, UPDATE ON shop.* TO 'shop'@'10.0.4.18';
FLUSH PRIVILEGES;

-- Verify what that row can actually do
SHOW GRANTS FOR 'shop'@'10.0.4.18';

-- Rotate every host row for the user, not just one
ALTER USER 'shop'@'10.0.4.18' IDENTIFIED BY 'new-secret-1';
ALTER USER 'shop'@'10.0.4.%' IDENTIFIED BY 'new-secret-1';
FLUSH PRIVILEGES;
📊 Production Insight
A hand-edited grant table on one replica diverged for a week because nobody flushed. The promotion then swapped in stale privileges and checkout 1045'd. Direct table edits plus a mandatory FLUSH in the runbook closed it.
🎯 Key Takeaway
Include the host part in every GRANT, verify with SHOW GRANTS FOR the exact row, and FLUSH after any manual grant-table write.

Test the Login Exactly the Way Your App Does

Most 1045 debugging fails on fidelity: engineers test from laptops and bastions, whose source IPs match different rows than the app fleet. Reproduce with the same protocol, host, port, and credential source the app uses. If the app connects over TCP to db-primary:3306, your test is mysql -u shop -p -h db-primary -P 3306 -e 'SELECT 1' — not a socket login from localhost, which exercises a different row entirely.

Pull the credential from the same secret store the app reads rather than retyping it; typos in hand-copied passwords have burned hours that a vault read would have avoided. Watch for DSN traps too: special characters like @, /, and # in passwords must be URL-encoded in connection strings, and some frameworks silently truncate at an unescaped delimiter — producing a deterministic 1045 that looks exactly like a wrong password.

Finally, check the server side while you test. A tail on the error log shows the source IP MySQL actually saw, which settles every 'but I created that row' argument instantly. If the log's IP and your row's host don't match, stop touching the password — you're fixing the wrong row. If the app runs in containers, exec into a running container for the test, since its source IP and DNS path can differ from the node's.

repro_1045.shBASH
1
2
3
4
5
6
7
8
9
10
11
# Reproduce over TCP exactly like the app (not via socket)
mysql -u shop -p -h db-primary -P 3306 -e 'SELECT CURRENT_USER();'

# Server health independent of credentials
mysqladmin -h db-primary ping

# What source IP did the server actually see?
sudo grep 'Access denied' /var/log/mysql/error.log | tail -20

# Confirm the client speaks the server's plugin
mysql --version
💡Copy the Secret, Don't Retype It
Read the password from the same vault path the app uses and pipe it in. Hand-typed secrets with @ or # break DSN parsing and fake a wrong-password 1045.
📊 Production Insight
A runbook smoke test passed from the bastion for months while the app row rotted — different source IPs, different rows. Moving the check into an app container caught the next rotation failure in 30 seconds.
🎯 Key Takeaway
Reproduce over the same protocol and source network as the app, read the secret from the vault, and confirm the server's logged source IP.

Stop It Recurring: Provisioning, Rotation, and Least Privilege

Individual 1045s are debugging; repeated 1045s are a provisioning gap. The durable fix is managing database users like code: CREATE USER and GRANT statements checked into version control, applied by the same pipeline that deploys the app, with one file per service account. When the fleet's subnets change, the user file changes in the same pull request — never as a forgotten manual step.

Rotation needs the same discipline. Rotate all host rows for the account in one job, verify a real login from an app host, and keep the previous secret valid until the fleet has converged — dual-secret support or a short overlap window prevents the all-red cliff. Log failed logins into your alerting (a spike of 1045s is often the first sign of a bad deploy or a brute-force probe), and never hand the app root or SUPER: a least-privilege account that leaks can only touch its own schema.

The deploy gate below belongs in CI: it fails the pipeline when the app can't authenticate before traffic shifts. Five seconds of check saves thirty-eight minutes of incident. Wire it into the same job that migrates the schema so credentials and schema always move together. Review the user file quarterly too: stale rows for decommissioned subnets become tomorrow's 1045 during the next failover.

deploy_gate_1045.shBASH
1
2
3
4
5
6
# Deploy gate: fail the pipeline if the app identity can't log in
set -euo pipefail
PASS="$(vault kv get -field=password secret/shop/db)"
MYSQL_PWD="$PASS" mysql -u shop -h db-primary -e 'SELECT 1;' shop
MYSQL_PWD="$PASS" mysql -u shop -h db-primary -e 'SHOW GRANTS;' | grep -q 'ON `shop`' \
  && echo GATE-PASS || (echo GATE-FAIL; exit 1)
📊 Production Insight
After codifying users in Terraform-managed SQL files and gating deploys on a live login, a team went from three 1045 incidents a quarter to zero in nine months — rotations became boring, which is the goal.
🎯 Key Takeaway
Manage users as code, rotate every host row with overlap, alert on 1045 spikes, and gate deploys on a live app-identity login.
● Production incidentPOST-MORTEMseverity: high

A 1:40 AM Password Rotation Locked Checkout Out for 38 Minutes

Symptom
At 1:40 AM the on-call rotated the shop database password via runbook. Within 60 seconds the checkout error rate went from zero to 100% — 214 failed logins per minute in the MySQL error log, all ERROR 1045 for 'shop'@'10.0.4.18'. Cart, search, and admin panels stayed green because they use different accounts. Payment webhooks queued up: 1,900 orders stuck in 'pending' by 2:00 AM. The rotation runbook's own smoke test passed, which delayed the rollback decision by 15 minutes.
Assumption
The team assumed the new password had a typo or hadn't replicated, so they re-ran the rotation twice and waited for replica lag to settle. The smoke test passed because it ran mysql -u shop -h db-primary from the bastion — which matched 'shop'@'bastion.internal', a row that had received the new password. Nobody compared the host part against the app servers' actual source IP, 10.0.4.18.
Root cause
The rotation script ran ALTER USER 'shop'@'10.0.4.19' IDENTIFIED BY 'new-secret' — a stale IP from before last month's autoscaling-group recycle. The app fleet now lived on 10.0.4.18, whose row 'shop'@'10.0.4.18' still held the old hash. Worse, the script never ran FLUSH PRIVILEGES after a direct grant-table touch on one replica, so behavior differed per node. The app presented the new password to a row expecting the old one: clean, total 1045.
Fix
They created the missing row explicitly — CREATE USER 'shop'@'10.0.4.18' IDENTIFIED WITH mysql_native_password BY 'new-secret'; GRANT SELECT, INSERT, UPDATE ON shop.* TO 'shop'@'10.0.4.18'; FLUSH PRIVILEGES — then rolled the app secret forward instead of back, since other services already used it. Checkout recovered in 4 minutes. The runbook was rewritten to resolve the app's live source IP (SELECT USER(), CURRENT_USER() from an app host), rotate every 'shop' host row, and fail the deploy if the smoke test doesn't run from an actual app container.
Key lesson
  • Always rotate by account row, not by username: list every 'user'@'host' row with SELECT user, host FROM mysql.user WHERE user='shop' and update each one, because MySQL stores separate passwords per host row.
  • Smoke-test from the app's network identity, not the bastion: a login that succeeds from one source IP proves nothing about the host row your fleet actually matches.
  • Make the runbook verify instead of assume: assert the new login works with mysql -u shop -p -h db-primary -e 'SELECT 1' from an app host before closing the rotation ticket.
Production debug guideFive checks in order — each one rules out a whole class of cause before you touch passwords.5 entries
Symptom · 01
App log shows Access denied for 'shop'@'10.0.4.18' (using password: YES)
→
Fix
List the exact candidate rows from a privileged session: SELECT user, host, plugin FROM mysql.user WHERE user='shop'; If no row's Host matches 10.0.4.18 or a covering pattern, you've found it — create the row. Then compare identities with SELECT USER(), CURRENT_USER(); when CURRENT_USER() isn't the account you expected, an anonymous or more-specific row is shadowing yours.
Symptom · 02
Row exists and password looks right, but logins still fail after a MySQL 8 upgrade
→
Fix
Check the plugin column: SELECT user, host, plugin FROM mysql.user WHERE user='shop'; If it says caching_sha2_password and your client predates support (check with mysql --version or php -v plus mysqlnd info), align it: ALTER USER 'shop'@'10.0.4.18' IDENTIFIED WITH mysql_native_password BY 'actual-secret'; then FLUSH PRIVILEGES; and retest from the app host, not your laptop.
Symptom · 03
You're not sure the password in the vault matches what's deployed
→
Fix
Reproduce exactly like the app: mysql -u shop -p -h db-primary -e 'SELECT 1'; If that fails, set it deterministically: ALTER USER 'shop'@'10.0.4.18' IDENTIFIED BY 'new-secret'; FLUSH PRIVILEGES; Update the secret store in the same change window. Don't test with -h localhost when the app uses TCP — socket connections match 'user'@'localhost' while TCP matches the IP row.
Symptom · 04
Grants look correct (SHOW GRANTS passes) yet the app still gets 1045
→
Fix
Run SHOW GRANTS FOR 'shop'@'10.0.4.18'; — note the full host part, since SHOW GRANTS FOR 'shop' alone can show a different row. Then hunt for shadowing anonymous accounts: SELECT user, host FROM mysql.user WHERE user=''; Any ''@'...' row matching your source network steals the match; remove leftovers with DROP USER ''@'10.0.4.%'; followed by FLUSH PRIVILEGES;
Symptom · 05
1045 appears only from some app hosts or only after a deploy
→
Fix
Check name resolution: MySQL sees the reverse-DNS name unless skip_name_resolve is ON (SHOW VARIABLES LIKE 'skip_name_resolve';). A host row for the IP won't match if the server resolves to a hostname. Standardize on IPs with skip_name_resolve=ON, or create both rows. Also grep the log for the real source: grep 'Access denied' /var/log/mysql/error.log | tail -20, and confirm the IP matches the row you rotated.
MySQL 1045 Causes at a Glance
Root CauseHow to ConfirmFixPrevention
No row for your user@hostSELECT user, host FROM mysql.user shows no matching Host for the logged IPCREATE USER 'u'@'source' + GRANT + FLUSHProvision one row per fleet subnet in code
Plugin mismatch (caching_sha2 vs native)plugin column disagrees with client; CLI works but app failsALTER USER ... IDENTIFIED WITH matching pluginPin plugin per row; upgrade legacy connectors
Wrong or stale passwordVault value fails via mysql -u -p -h TCP reproALTER USER on every host row; update vaultRotate all rows atomically with overlap
Anonymous ''@... row shadowingCURRENT_USER() differs from USER(); '' rows existDROP USER ''@'pattern'; FLUSH PRIVILEGESDelete anonymous rows on every provision
Socket vs TCP row confusion(using password: NO) or localhost works but TCP failsCreate both 'localhost' and IP rows as neededTest with the same protocol the app uses
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
diagnose_1045.sqlSELECT user, host, plugin FROM mysql.user WHERE user = 'shop';Read the Error Line Like a Log, Not a Complaint
create_user_host.sqlCREATE USER 'shop'@'10.0.4.18' IDENTIFIED WITH mysql_native_password BY 'change-...MySQL Users Are user@host Pairs, Not Bare Names
fix_plugin_1045.sqlSELECT user, host, plugin FROM mysql.user WHERE user = 'shop';Auth Plugin Mismatch
grants_1045.sqlGRANT SELECT, INSERT, UPDATE ON shop.* TO 'shop'@'10.0.4.18';FLUSH PRIVILEGES and GRANT Syntax That Actually Works
repro_1045.shmysql -u shop -p -h db-primary -P 3306 -e 'SELECT CURRENT_USER();'Test the Login Exactly the Way Your App Does
deploy_gate_1045.shset -euo pipefailStop It Recurring

Key takeaways

1
Error 1045 is authentication, not networking
parse the user, host, and password YES/NO before acting.
2
Accounts are user@host pairs
provision, rotate, and verify every row your fleet matches.
3
Check the plugin column early
caching_sha2_password breaks legacy clients with a perfect password.
4
SHOW GRANTS FOR the exact row, and run FLUSH PRIVILEGES after any manual grant-table write.
5
Reproduce over the same protocol and source IP as the app, reading the secret from the vault.
6
Manage users as code and gate deploys on a live app-identity login to end repeat 1045s.

Common mistakes to avoid

5 patterns
×

Running the app as root and calling it done

Symptom
No 1045 today, but a leaked credential exposes every schema and SUPER lets attackers load plugins.
Fix
Create a least-privilege account per service with schema-scoped GRANTs; reserve root for break-glass via the bastion.
×

Creating only 'app'@'%' and assuming localhost is covered

Symptom
CLI over socket works, app over TCP fails — or vice versa — with identical passwords.
Fix
Create both 'app'@'localhost' and the TCP row your fleet uses; verify each with its own protocol.
×

Hand-editing mysql.user and skipping FLUSH PRIVILEGES

Symptom
The row looks right in the table but logins behave as before, differently per replica.
Fix
Prefer ALTER USER / GRANT (auto-reload); after any direct table write, run FLUSH PRIVILEGES on every node.
×

Downgrading the whole server to mysql_native_password

Symptom
Legacy app recovers but every new account inherits the weaker handshake.
Fix
Scope the exception to the legacy row only; keep caching_sha2_password default and ticket the connector upgrade.
×

Pasting passwords with @ or # into DSN strings unescaped

Symptom
Deterministic 1045 that survives resets; works in CLI, fails only in the app.
Fix
URL-encode special characters in connection strings or, better, pass the secret via env var / vault agent.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does MySQL error 1045 mean, and what does (using password: YES/NO) ...
Q02SENIOR
Why can SHOW GRANTS look fine while the app still gets 1045?
Q03SENIOR
USER() and CURRENT_USER() return different values. What does that prove?
Q04SENIOR
A MySQL 8 upgrade broke a legacy PHP app with 1045 while the CLI still l...
Q05SENIOR
A new replica 1045s for an account that works on the primary. Walk throu...
Q01 of 05JUNIOR

What does MySQL error 1045 mean, and what does (using password: YES/NO) tell you?

ANSWER
It's an authentication rejection, not a connectivity failure. YES means the client offered a password that didn't match the matched row; NO means it offered none — pointing at an empty secret or dropped DSN field rather than a wrong password.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How is error 1045 different from 1049 or 2003?
02
Do I always need FLUSH PRIVILEGES after CREATE USER or GRANT?
03
Why does 'app'@'%' not cover my localhost connection?
04
How do I safely reset a lost app password?
05
Can skip_name_resolve cause 1045?
06
Should I just enable mysql_native_password server-wide for old apps?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Written from production experience, not tutorials.

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

That's MySQL. Mark it forged?

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

←
Previous
SQL Top N per Group Greatest N per Group
1 / 7 · MySQL
Next
MySQL Lock Wait Timeout Fix
→