psql: FATAL: password authentication failed for user - 10 Causes and Fixes
psql: FATAL: password authentication failed for user means the PostgreSQL server received your connection, looked up the role you asked for, compared the password you sent against the stored verifier, and rejected the match. The connection itself worked — TCP opened, TLS negotiated, the startup packet was parsed — so psql: FATAL: password authentication failed for user is never a networking problem, and chasing firewalls will waste your afternoon.
The deceptive part is that the server prints the same message for a wrong password, a role that does not exist, a role with no password set at all, and a password that exists but was hashed with an algorithm your client cannot speak. That is deliberate: PostgreSQL refuses to tell an anonymous caller which of those is true.
This page gives you the fastest fix first, then ten distinct causes ordered by how often they actually bite in production, then a checklist for the case where none of them match. Every cause includes the command that reproduces it and the command that proves it is gone.
The server-side log is the one place where the ambiguity disappears. If you can reach postgresql.log, read it before you read anything else here.
| Error | psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: password authentication failed for user "app_user" |
|---|---|
| Where it happens | PostgreSQL 9.x through 17.x - psql, libpq, and every driver built on libpq (psycopg2/psycopg, node-postgres, pgx, JDBC, Rails, Django, SQLAlchemy). Appears at connection time, before any query runs. |
| What it means | The server found your connection but rejected your credentials - either the password is wrong, the role has no password, or the client and server disagree on how to hash it. |
The Fast Fix
Set the password explicitly on the server, then connect with it passed through the environment rather than typed by hand.
# On the DB host, as a superuser
sudo -u postgres psql -c "ALTER ROLE app_user WITH PASSWORD 'new_secret';"
# From the client
PGPASSWORD='new_secret' psql -h localhost -U app_user -d app_db
ALTER ROLE ... WITH PASSWORD rewrites the stored verifier using whatever password_encryption the server is currently set to, which repairs both a wrong password and a role created without one.
If that still fails, the server is matching a different pg_hba.conf line than you expect — jump to the diagnostic checklist.
What Is Actually Causing It
Jump to your case
- 1. The password is simply wrong or has a shell-mangled character
- 2. The role exists but has no password set
- 3. pg_hba.conf matches a different line than you assume
- 4. Server stores a SCRAM verifier but the client library is too old
- 5. password_encryption was changed after the password was set
- 6. A stale ~/.pgpass or PGPASSWORD overrides what you type
- 7. The role name is case-sensitive and you are sending the folded form
- 8. pg_hba.conf was edited but never reloaded
- 9. A Docker Postgres volume kept the old POSTGRES_PASSWORD
- 10. A connection pooler such as PgBouncer holds its own credential list
1. The password is simply wrong or has a shell-mangled character
Reproduce it
# Password is actually p@ss!word
psql -h localhost -U app_user -d app_db
# Password for user app_user: p@ss!word <- typed, but history expansion ate it
# psql: error: ... FATAL: password authentication failed for user "app_user"
# Or passed inline in double quotes - ! and $ are expanded by the shell
PGPASSWORD="p@ss!word" psql -h localhost -U app_user
Why it happens — In an interactive bash shell, ! triggers history expansion and $ triggers variable substitution inside double quotes. The string libpq sends is not the string you typed, so the server compares the wrong bytes and rejects them.
The fix
# Single quotes: no expansion of ! or $
PGPASSWORD='p@ss!word' psql -h localhost -U app_user -d app_db
# Or take it out of the command line entirely
read -rs PGPASSWORD && export PGPASSWORD
psql -h localhost -U app_user -d app_db
Changed: single quotes instead of double, or read the value into the variable so no shell parsing touches it.
Confirm it worked — Run printf '%s\n' "$PGPASSWORD" | cat -A and confirm the characters match the password exactly, with no stray $ expansion and a single trailing $ line marker.
2. The role exists but has no password set
Reproduce it
-- Role created without a password
CREATE ROLE app_user LOGIN;
psql -h localhost -U app_user -d app_db
# Password for user app_user: (anything)
# psql: error: ... FATAL: password authentication failed for user "app_user"
Why it happens — CREATE ROLE ... LOGIN with no PASSWORD clause stores a NULL verifier. When pg_hba.conf demands scram-sha-256 or md5, a NULL verifier can never match, so every password fails — including an empty one.
The fix
ALTER ROLE app_user WITH PASSWORD 'new_secret';
Changed: the role now has a stored verifier for the auth method to compare against.
Confirm it worked — Run SELECT rolname, rolpassword IS NOT NULL AS has_password FROM pg_authid WHERE rolname = 'app_user'; as a superuser. It must return t.
3. pg_hba.conf matches a different line than you assume
Reproduce it
# pg_hba.conf - first match wins, top to bottom
host all all 127.0.0.1/32 trust
host app_db app_user 127.0.0.1/32 scram-sha-256
# You connect over the Unix socket, not TCP - neither host line applies
psql -U app_user -d app_db
# psql: error: FATAL: password authentication failed for user "app_user"
Why it happens — PostgreSQL scans pg_hba.conf top to bottom and uses the first line whose connection type, database, user, and address all match. It does not fall through to a later line when authentication fails. Omitting -h makes psql use the Unix socket, which only local lines cover.
The fix
# Force TCP so the host lines apply
psql -h 127.0.0.1 -U app_user -d app_db
# Or add the local line, placed above any broader local rule
local app_db app_user scram-sha-256
Changed: the connection type now matches the rule you intended.
Confirm it worked — On PostgreSQL 10+, run SELECT line_number, type, database, user_name, address, auth_method FROM pg_hba_file_rules ORDER BY line_number; as a superuser and confirm which line matches your connection type and address.
4. Server stores a SCRAM verifier but the client library is too old
Reproduce it
# Server: PostgreSQL 14, password_encryption = scram-sha-256
# Client: libpq 9.6 or psycopg2 built against it
psql -h db.internal -U app_user -d app_db
# psql: FATAL: password authentication failed for user "app_user"
# psycopg2 wheel linked against an old libpq
import psycopg2
psycopg2.connect(host="db.internal", user="app_user", password="secret", dbname="app_db")
# psycopg2.OperationalError: FATAL: password authentication failed for user "app_user"
Why it happens — SCRAM-SHA-256 authentication landed in PostgreSQL 10. A client built on libpq 9.6 or earlier does not implement the SASL exchange, so it cannot complete the handshake and the server reports a plain authentication failure.
The fix
# Upgrade the client
psql --version # want 10 or newer, ideally matching the server major
# Python: use the binary wheel that bundles a modern libpq
pip install --upgrade 'psycopg[binary]'
Changed: the client now speaks SASL/SCRAM instead of md5-only.
Confirm it worked — Run psql --version on the client and confirm 10+, then check the server side with SHOW server_version;. In the server log, a successful attempt logs connection authenticated: ... method=scram-sha-256.
5. password_encryption was changed after the password was set
Reproduce it
-- Password stored while password_encryption = md5
ALTER ROLE app_user WITH PASSWORD 'secret';
SHOW password_encryption; -- md5
# Then postgresql.conf is switched and pg_hba.conf tightened
password_encryption = scram-sha-256
# pg_hba.conf
host all all 0.0.0.0/0 scram-sha-256
Why it happens — Changing password_encryption only affects passwords set after the change. The existing verifier is still an md5 hash, and a scram-sha-256 HBA line cannot validate an md5 verifier, so every login fails until the password is re-set.
The fix
-- Re-set the password so it is re-hashed with the current algorithm
ALTER ROLE app_user WITH PASSWORD 'secret';
Changed: the verifier is regenerated under scram-sha-256; the password value itself can stay the same.
Confirm it worked — Run SELECT rolname, left(rolpassword, 14) FROM pg_authid WHERE rolname = 'app_user'; — a SCRAM verifier starts with SCRAM-SHA-256$, an md5 one starts with md5.
6. A stale ~/.pgpass or PGPASSWORD overrides what you type
Reproduce it
cat ~/.pgpass
# localhost:5432:*:app_user:old_password
psql -h localhost -U app_user -d app_db
# No prompt appears at all
# psql: error: ... FATAL: password authentication failed for user "app_user"
Why it happens — libpq resolves the password in a fixed order: the connection string, then PGPASSWORD, then the password file. If any of those yields a value, psql never prompts you, so you can sit there certain you typed the right password when psql never asked.
The fix
# Update the entry
sed -i.bak 's/old_password/new_secret/' ~/.pgpass
chmod 600 ~/.pgpass
# Or bypass both sources for one connection
env -u PGPASSWORD PGPASSFILE=/dev/null psql -h localhost -U app_user -d app_db
Changed: the stale credential source is corrected or removed from the lookup chain.
Confirm it worked — If psql now shows a Password for user app_user: prompt, no file or env var is supplying a value. Also confirm ~/.pgpass is mode 0600 — libpq silently ignores a world-readable file.
More causes (4 remaining)
7. The role name is case-sensitive and you are sending the folded form
Reproduce it
CREATE ROLE "AppUser" LOGIN PASSWORD 'secret';
psql -h localhost -U AppUser -d app_db
# Works - psql passes the name through verbatim
psql -h localhost -U appuser -d app_db
# psql: error: FATAL: password authentication failed for user "appuser"
Why it happens — Unquoted identifiers in SQL fold to lowercase, but a name in double quotes preserves its case. The -U flag sends the literal string, so appuser and AppUser are two different roles — and the nonexistent one reports the same password failure rather than admitting it does not exist.
The fix
-- Prefer an all-lowercase role name
ALTER ROLE "AppUser" RENAME TO app_user;
ALTER ROLE app_user WITH PASSWORD 'secret';
Changed: the role name no longer depends on quoting, so every client spells it the same way.
Confirm it worked — Run SELECT rolname FROM pg_roles ORDER BY 1; as a superuser and compare the exact string against your -U value, character for character.
8. pg_hba.conf was edited but never reloaded
Reproduce it
# Edit the file
sudo vim /etc/postgresql/16/main/pg_hba.conf
# change scram-sha-256 -> trust on the local line
psql -h 127.0.0.1 -U app_user -d app_db
# psql: error: FATAL: password authentication failed for user "app_user"
Why it happens — The postmaster reads pg_hba.conf into memory at startup and re-reads it only on SIGHUP. Until you reload, the running server keeps enforcing the old rules regardless of what the file on disk says.
The fix
sudo systemctl reload postgresql
# or, from inside a session
psql -U postgres -c "SELECT pg_reload_conf();"
Changed: the postmaster re-parses the HBA file and the new rules take effect for the next connection.
Confirm it worked — On PostgreSQL 10+, SELECT * FROM pg_hba_file_rules; reflects the loaded file and lists an error column — a non-null value there means the line was rejected and is not in force.
9. A Docker Postgres volume kept the old POSTGRES_PASSWORD
Reproduce it
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: new_secret # changed from old_secret
volumes:
- pgdata:/var/lib/postgresql/data
docker compose up -d
psql -h localhost -U postgres
# Password: new_secret
# psql: error: FATAL: password authentication failed for user "postgres"
Why it happens — The official image only runs its initialization — including applying POSTGRES_PASSWORD — when the data directory is empty. An existing named volume means initdb is skipped entirely and the container keeps the password baked in on first run.
The fix
# Option A: change the password in the running container
docker compose exec db psql -U postgres -c "ALTER ROLE postgres PASSWORD 'new_secret';"
# Option B: discard the volume and re-initialize (destroys all data)
docker compose down -v && docker compose up -d
Changed: the password is updated in the existing cluster instead of relying on init that will never run again.
Confirm it worked — docker compose exec db psql -U postgres -c 'SELECT 1;' succeeds, and docker compose logs db | grep -i 'database system is ready' shows no initdb block on the most recent start.
10. A connection pooler such as PgBouncer holds its own credential list
Reproduce it
; userlist.txt - still the pre-rotation hash
"app_user" "md5abc123..."
psql -h pgbouncer.internal -p 6432 -U app_user -d app_db
# psql: error: FATAL: password authentication failed for user "app_user"
Why it happens — PgBouncer authenticates the client against its own user list before it opens or reuses a server connection. Rotating the password in PostgreSQL leaves the pooler's copy stale, so the failure happens at the pooler and never reaches the database.
The fix
# Regenerate the pooler's credential entry, then reload it
pgbouncer -R -d /etc/pgbouncer/pgbouncer.ini
# or
sudo systemctl reload pgbouncer
Changed: the pooler now holds the current verifier for the role.
Confirm it worked — Connect straight past the pooler on port 5432 — if that succeeds and port 6432 fails, the pooler's credential store is the culprit. Confirm in the PgBouncer log, which records the auth failure independently of the PostgreSQL log.
- Read the server log, not the client message:
sudo tail -50 /var/lib/pgsql/data/log/postgresql-*.log(or/var/log/postgresql/). A line readingrole "x" does not existmeans you have a name or case problem, not a password problem;password authentication failedalone means the role exists and the verifier did not match. - Confirm you are hitting the server you think you are:
psql -h <host> -U postgres -c 'SELECT inet_server_addr(), inet_server_port(), current_database();'. A staging DSN in your shell environment produces exactly this error against a role that has a different password there. - Rule out the credential-lookup chain: run
env -u PGPASSWORD PGPASSFILE=/dev/null psql -h <host> -U <role>. If a password prompt now appears where it did not before, a stalePGPASSWORDor~/.pgpasswas answering for you. - Determine which HBA line actually matched:
SELECT line_number, type, database, user_name, address, auth_method, error FROM pg_hba_file_rules;(PostgreSQL 10+). Compare against your connection type — omitting-hmeanslocal, nothost. A non-nullerrorcolumn means that line is not in force at all. - Check whether the role has a password and how it is hashed:
SELECT rolname, left(rolpassword, 14) FROM pg_authid WHERE rolname = '<role>';. NULL rules in no password set; a leadingmd5against ascram-sha-256HBA line rules in algorithm mismatch. - Compare client and server capability:
psql --versionon the client versusSHOW server_version;. A client older than 10 against a SCRAM-only server can never authenticate, no matter how correct the password is. - Isolate the pooler: connect directly to port 5432, bypassing PgBouncer or RDS Proxy. Success there rules the database in and the pooler's credential store out.
- As a last resort, prove the password path end to end: temporarily set the matching
pg_hba.confline totrust, runpg_reload_conf(), connect,ALTER ROLE ... WITH PASSWORD, then restore the original line and reload again. Iftrustalso fails, the problem is the role name or the target server, not the password.
Why This Error Exists At All
PostgreSQL deliberately collapses several different failures into one message. A wrong password, a role that does not exist, a role with a NULL verifier, and a role whose stored hash cannot be validated by the negotiated method all produce the same FATAL: password authentication failed for user. That is not sloppy error handling — it is an anti-enumeration measure. If the server distinguished "no such role" from "wrong password", an anonymous caller could harvest valid role names one probe at a time. The detail is not discarded, just relocated: the server log records the precise reason, and reading the log is gated behind filesystem access you have to already possess.
The second design decision is that authentication is configuration-driven, not role-driven. A role does not carry "authenticate me with SCRAM". Instead pg_hba.conf maps a tuple of (connection type, database, role, source address) to a method, and the server picks the first matching line and stops. There is no fallthrough on failure. This is why the same credentials succeed over the Unix socket and fail over TCP, or succeed from 127.0.0.1 and fail from a container's bridge IP: you did not change the password, you changed which line matched. Once that clicks, no pg_hba.conf entry for host ... and this error stop looking like separate problems — they are the same lookup, one missing a line and one finding a line you did not intend.
The third piece is that the stored verifier and the wire protocol are separate layers that must agree. password_encryption decides what gets written into pg_authid at the moment CREATE ROLE or ALTER ROLE ... PASSWORD executes; pg_hba.conf decides what challenge the server issues at connection time. Neither retroactively rewrites the other. Flip password_encryption from md5 to scram-sha-256 and every existing password keeps its md5 verifier until someone re-issues ALTER ROLE. This is the entire family of "the password is correct but it still fails" bugs, and the fix is always the same shape: re-set the password so the verifier is regenerated under the algorithm currently in force.
Stop It From Coming Back
- Set
password_encryption = scram-sha-256inpostgresql.confbefore creating any roles, so no md5 verifier ever exists to go stale. Re-issueALTER ROLE ... WITH PASSWORDfor every existing role in the same maintenance window. - Keep
pg_hba.confin version control and validate it after every edit withSELECT * FROM pg_hba_file_rules WHERE error IS NOT NULL;— that query catches typos and bad CIDR masks before the next deploy discovers them. - Use
~/.pgpasswith mode0600or a secret manager instead ofPGPASSWORDin shell history and CI logs. libpq ignores a password file with looser permissions, which turns a silent leak into a visible failure. - Add a connectivity smoke test to CI or container startup:
psql "$DATABASE_URL" -c 'SELECT 1;'before the app boots, so credential drift surfaces as an explicit failed step rather than a runtime connection storm. - Never rely on
POSTGRES_PASSWORDto update an existing Docker volume. Rotate credentials with an explicitALTER ROLEstep in your deployment scripts, and treatdocker compose down -vas a data-destroying command. - Pin the client libpq major version alongside the server in your image builds, and assert it in CI with
psql --version, so a base-image change cannot silently reintroduce a pre-SCRAM client.
Errors You Will Probably Hit Next
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: no pg_hba.conf entry for host "127.0.0.1", user "app_user", database "app_db", no encryptionpsql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: role "app_user" does not existpsql: error: connection to server at "db" (172.18.0.2), port 5432 failed: Connection refused - Is the server running on that host and accepting TCP/IP connections?
Treat this error as a lookup failure, not a typing failure. Three things have to line up — the role name you send, the pg_hba.conf line your connection type and source address actually match, and the algorithm the stored verifier was written with — and the message tells you which one broke only in the server log. Check the log first, then pg_hba_file_rules, then pg_authid; retyping the password is the last thing to try, not the first.
댓글
댓글 쓰기