Surreal illustration of a duck at a ticket window speaking with a goose wearing military uniform, surrounded by warning signs in Cyrillic text inside a concrete building.

Both of these parameters are a call to sleep() with a GUC attached. A newly forked backend stops for the configured number of seconds so that a developer can find its PID and attach a debugger before the interesting part begins. pre_auth_delay sleeps immediately after the fork, before the backend has read a byte from the client. post_auth_delay sleeps once authentication is finished and the session’s settings have been loaded. (The alphabet puts them in the wrong order. I’m taking them in the order a connection meets them.)

Each is an integer number of seconds, and each defaults to 0, which means no delay. Both live under Developer Options, a section of the documentation that opens by saying its contents “should never be used on a production database,” and neither appears in postgresql.conf.sample. So this post is less about how to set them than about what a server looks like when somebody has.

Before

pre_auth_delay has context sighup and a range of 0 to 60. It arrived in PostgreSQL 7.2, in the same September 2001 commit that created authentication_timeout, and its definition has not changed since. It exists for debugging authentication itself, the earliest stretch of a session’s life and one you cannot reach by connecting first and asking for pg_backend_pid(). The delay does not count against authentication_timeout; that clock starts afterwards.

While it sleeps, the new process is close to invisible. It has no row in pg_stat_activity, it has not logged connection received, and it has not yet set its process title, so in ps it looks like a second postmaster. That is how you find it:

1$ ps -o pid,args --ppid $(head -1 $PGDATA/postmaster.pid) | grep -v "postgres: "
2 PID COMMAND
3 8997 /usr/lib/postgresql/18/bin/postgres -D /var/lib/postgresql/18/main -c config_file=...
4$ gdb -p 8997
5(gdb) break ClientAuthentication
6(gdb) continue
7Breakpoint 1, ClientAuthentication (port=0x55fcd72ca4e0) at .../src/backend/libpq/auth.c:380
8(gdb) print port->user_name
9$1 = 0x55fcd7377930 "alice"

That is the entire feature, and on a development machine it is a perfectly good one.

On a server other people use, it is a tax on every incoming connection. Replication connections pay it. So do the connections that are about to be told sorry, too many clients already, which wait out the full delay before hearing the bad news. So do cancel requests, which travel on a connection of their own: at pre_auth_delay = 5, Ctrl-C in psql took five seconds to stop the query. And pg_isready (default timeout: three seconds) reports no response and exits with status 2, so anything using it as a health check now believes the server is down.

A backend this young also has every signal the postmaster would use to stop it politely blocked; only SIGQUIT gets through. I requested a fast shutdown two seconds after a client connected to a server with pre_auth_delay = 30:

111:39:40.588 LOG: received fast shutdown request
211:39:40.589 LOG: aborting any active transactions
311:40:08.574 LOG: shutting down

Twenty-eight seconds of nothing, while the postmaster waited on a process that had not yet said hello to anyone. (An immediate shutdown does not wait.)

After

post_auth_delay has the rare backend context, which it shares with exactly one other parameter, ignore_system_indexes. It can be set in postgresql.conf, where a reload affects new sessions only, or by the client as part of the connection request, and never afterwards. SET fails; so does ALTER ROLE ... SET. The connection-time form requires no privileges: an ordinary role connecting with PGOPTIONS='-c post_auth_delay=5' got its first result 5.07 seconds later. (PGOPTIONS='-W 5' does the same thing. -W is the original server switch; the parameter name was attached to it in 8.2.)

This sleep comes late enough that the backend holds a connection slot and has its final settings, and early enough that session_preload_libraries have not been loaded and login event triggers have not fired. That is the reason to want it: those are things that happen before you can type anything.

Two things about it are not in the documentation.

The first is that a client connection cannot reach the advertised maximum of 2147 seconds. The sleep is not resumed if a signal interrupts it, and one always does. The kernel timer armed for authentication_timeout when the backend starts listening to its client is never cancelled (cancelling it would cost a system call, and the handler knows to ignore a stale alarm), so it goes off in the middle of the nap. post_auth_delay = 90 on a default server hands the session over 60.0 seconds after it connected; I measured that on 14, 17, 18, and 19 beta 4. The practical ceiling is authentication_timeout, whose own maximum is 600, less however long the client took to connect and log in. A pg_reload_conf() in the meantime ends the sleep early too, for every sleeping client backend at once. If you need more than a minute to get attached, raise both.

The second is what happens when it is set server-wide. The sleep also applies to the autovacuum launcher, autovacuum workers, and background workers, which never authenticate and whose PIDs you cannot ask for in advance. For those, only the value in the server configuration counts: a parallel worker has finished sleeping before it learns anything about its leader’s settings, so a post_auth_delay supplied by the client never reaches it. If you need to be attached to a parallel worker before it does anything interesting, the setting has to go in postgresql.conf, where everyone pays for it. Workers that connect to a database pass through the sleep twice, once in their own startup code and once in the code they share with ordinary backends, and they have no authentication timer to cut it short. With post_auth_delay = 3 in the configuration, a two-worker parallel count(*) that had taken 108 ms took 6,010 ms. Sessions that had connected before the reload paid as well, since their workers are new processes.

Finding it

If either one has been left on, the server will admit to it:

1SELECT name, setting, source, sourcefile
2 FROM pg_settings
3 WHERE name IN ('pre_auth_delay', 'post_auth_delay');

A client that passes post_auth_delay in its own connection options does not show up there. PostgreSQL 18 gives you two ways to catch it. A sleeping backend appears in pg_stat_activity with state = 'starting' and no user or database, visible to superusers and members of pg_read_all_stats (through 17 it has no row at all, only a process title ending in startup). And with setup_durations in log_connections, the connection confesses when it finally completes:

1LOG: connection ready: setup total=5029.435 ms, fork=0.755 ms, authentication=11.943 ms

Five seconds of setup, of which forking and authenticating account for thirteen milliseconds. Both delays land in that gap.

Leave both at 0 on anything that is not your own development build. The one use I can see for either away from a debugger is PGOPTIONS='-c post_auth_delay=10' against a staging server, which produces a ten-second login on demand and shows you what your application’s connect timeout really does. (libpq with connect_timeout=3 gave up on schedule; the backend it abandoned slept out the remaining seven seconds, holding its connection slot.) And if production connections ever start taking a suspiciously round number of seconds, look here before you blame DNS.