Fix PostgreSQL "fe_sendauth: no password supplied" in Django

Fix the PostgreSQL error fe_sendauth no password supplied in Django: find why the password is empty, check DATABASE_URL parsing and environment loading in systemd, Docker, and cron, escape special characters, and use .pgpass or secrets safely.

Problem statement

Django starts, or a management command runs, and the first query fails:

django.db.utils.OperationalError: connection to server at "db.internal" (10.0.1.20),
port 5432 failed: fe_sendauth: no password supplied

The fe_ prefix means the message comes from the client library (libpq, the "frontend"), not the server. PostgreSQL asked for a password, and the client had none to send. The server is reachable and expects password authentication; Django simply passed an empty password. The question is where it got lost.

Where the password disappearsSecret storedenv file or vaultProcess startsenv not loaded?Settings buildPASSWORD is emptylibpqno password suppliedThe server asked for a password; the client had an empty string to send.
  1. Print what Django sees: python manage.py shell -c "from django.db import connection; print(bool(connection.settings_dict['PASSWORD']))".
  2. If it is empty, the environment variable is not loaded in this context: systemd, cron, Docker, or sudo each start with their own environment.
  3. With DATABASE_URL, URL-encode special characters in the password (@ becomes %40, / becomes %2F).
  4. Check that the settings read the variable you actually set (DB_PASSWORD versus POSTGRES_PASSWORD).
  5. For scripts and cron, a ~/.pgpass file supplies passwords without putting them on the command line.

Step-by-step solution

1. See whether Django has a password

Run the check in exactly the context that fails, such as inside the container or as the service user:

sudo -u deploy bash -lc 'cd /srv/app && .venv/bin/python manage.py shell -c "
from django.db import connection
s = connection.settings_dict
print(s[\"HOST\"], s[\"USER\"], \"password set:\", bool(s[\"PASSWORD\"]))"'

Never print the password itself into logs or shell history. If password set: False, keep reading. If it is set, the server is rejecting a wrong password, which produces a different message: see Fix PostgreSQL Password Authentication Failed.

2. Load the environment where the process runs

Each launcher has its own environment:

  • systemd ignores your shell profile. Use EnvironmentFile=/etc/myapp/env in the unit and make sure the file is readable by root (systemd reads it before dropping privileges).
  • cron runs with an almost empty environment. Source the file in the job or use a wrapper script.
  • sudo resets the environment by default; sudo -u deploy env shows what survives.
  • Docker Compose only passes variables listed under environment: or env_file: to the container.
is the password loaded here?ReadsLoad it withInteractive shell~/.bashrc, profileexport or .envsystemd service!unit onlyEnvironmentFile=cron job!almost nothingsource in wrapperDocker container!compose keys onlyenv_file: or secretsA variable exported in your shell exists only in your shelldjango-deployment.com
Test in the same launcher that fails, not in your login shell.

3. Check DATABASE_URL parsing

dj-database-url and django-environ split the URL on :, @, and /. A password containing those characters is cut short or ends up empty:

# broken: password contains @ and /
DATABASE_URL=postgres://app:p@ss/word@db.internal:5432/appdb
# fixed: percent-encode the password
DATABASE_URL=postgres://app:p%40ss%2Fword@db.internal:5432/appdb

Encode a password safely with Python:

python3 -c "import urllib.parse, getpass; print(urllib.parse.quote(getpass.getpass(), safe=''))"

4. Match the variable names

Settings files often read one name while deployment sets another:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ.get("DB_PASSWORD", ""),
        "HOST": os.environ["DB_HOST"],
    }
}

Here os.environ.get(..., "") hides the mistake by defaulting to an empty string. Prefer os.environ["DB_PASSWORD"] so a missing variable fails with a clear KeyError at startup.

5. Use .pgpass for scripts

Backup scripts and psql sessions can read passwords from ~/.pgpass instead of the environment:

# ~/.pgpass  (chmod 600)
db.internal:5432:appdb:app:the-password

libpq ignores the file if its permissions are wider than 0600. Django can use it too if PASSWORD is left out of settings, since psycopg passes the connection to libpq.

why the password is emptyWhere it failsCause and fixonly under systemdshell env not inherited;use EnvironmentFile=only in cronempty cron env; sourcethe env file in a wrapperpassword has @ or /DATABASE_URL splitearly; percent-encode iteverywheresettings read anothername; fail on missing varsThe server is fine; the client is sending an empty passworddjango-deployment.com
Make missing secrets fail loudly at startup instead of defaulting to empty strings.

Explanation

When pg_hba.conf assigns a password method such as scram-sha-256 to the connection, the server sends an authentication request. libpq looks for a password in the connection parameters, then in PGPASSWORD, then in the password file. If none of them has one, it gives up with fe_sendauth: no password supplied without contacting the server again. Because Django builds the connection parameters from settings, an empty PASSWORD key is enough to trigger it.

Edge cases / notes

  • Local development with trust auth. Projects that used trust locally never needed a password, so the setting was never wired up. The first deploy against a real server exposes it.
  • Secrets mounted as files. Docker and Kubernetes secrets are often files. Read them explicitly, for example with a _FILE variable convention, rather than expecting an environment variable.
  • PgBouncer. With auth_type = scram-sha-256 on the pooler, the same message can come from PgBouncer's side; check its auth_file.

Next steps

FAQ

What does fe_sendauth mean?

It is a message from libpq, the PostgreSQL client library. "fe" stands for frontend. It means the client had no password to send when the server asked for one.

Is this the same as password authentication failed?

No. That error means a password was sent and rejected. This one means no password was sent at all.

Why does it work in my terminal but not in the service?

Your terminal has the variable exported; the service starts with its own environment. Load the variables in the unit file or container definition.

Can I put the password directly in settings.py?

It works but leaks the secret into git. Use environment variables or a secrets manager and fail fast when they are missing.

Keep going