Skip to content

PostgreSQL Backend Lifecycle — Fork, Initialize, Command Loop, and Exit

Contents:

A database server that accepts concurrent client sessions must solve a fundamental isolation problem: each session needs its own execution context — its own stack, its own memory, its own view of system state — while also sharing the data structures every session touches (buffer pool, lock table, transaction catalog). Two architectural patterns dominate:

  1. Thread-per-session. One OS thread per connected client, sharing a single address space. State is isolated via thread-local variables; shared state is protected by mutexes. Examples: MySQL (InnoDB foreground threads), Oracle (dedicated server in threaded mode), SQL Server. The advantage is low fork overhead and tight sharing of memory; the risk is that a bug in one session’s code corrupts shared memory and crashes the entire server.

  2. Process-per-session. One OS process per connected client, each with its own address space. A crash in one backend leaves the server alive. Shared state lives in an explicitly mapped shared-memory segment that all processes attach to at startup. Examples: original PostgreSQL, original Oracle dedicated-server mode.

PostgreSQL uses process-per-session. The design choice traces to the 1986 “Design of POSTGRES” paper (Stonebraker & Rowe) and has been retained deliberately: process isolation confines a backend crash to that session and makes the code easier to reason about because most global C variables are per-process by default. The operating system enforces address-space separation, so there is no risk that a rogue backend can corrupt the buffer pool of a concurrent session.

The cost is fork overhead (mitigated by connection poolers like PgBouncer and, in PG17+, the built-in connection pool prototype) and the need for an explicit shared-memory design. Architecture of a Database System (Hellerstein et al., 2007, §2) covers both models and notes that “most modern database systems are multithreaded” while observing that “multiprocessing is simpler to implement correctly.” PostgreSQL’s choice reflects that latter preference, taken at a time when the simplicity dividend was decisive and maintained since because changing it would require rewriting every global-variable usage.

A process-per-session system must answer three additional questions that thread-per-session sidesteps:

  1. How does a new process acquire identity in shared state? It cannot inherit per-session structures from a parent (those are in the parent’s private heap); it must register itself in shared memory.
  2. How does it reach catalog data? Reading pg_class, pg_authid, and dozens of other catalogs requires a transaction context, a relation cache, a system catalog cache, and an open relation on the database directory — none of which a freshly forked process possesses.
  3. How does it recover from a per-statement error without dying? A kill(backend, SIGTERM) from the client cannot be allowed to crash the process mid-transaction; the backend must roll back the statement and return to the idle loop.

These three questions shape the entire initialization and command-loop design described in §“PostgreSQL’s Approach.”

Almost every database engine with a process-per-session model goes through the same four-phase arc, regardless of brand. Understanding the generic pattern makes PostgreSQL’s choices read as a specific instantiation of a well-known idiom.

Phase 1 — Process birth and environment setup

Section titled “Phase 1 — Process birth and environment setup”

The postmaster fork()s a child (or on Windows, spawns a new process via EXEC_BACKEND). The child inherits the postmaster’s open file descriptors and copies of its GUC state; it does not inherit any per-session structures, because those do not yet exist. The first action is to install signal handlers appropriate for a backend (not the postmaster), reset any inherited state that is only valid in the parent, and record the backend’s own PID.

Before a backend can use any shared subsystem (lock manager, buffer manager, procarray), it must register itself. Registration means: allocating a slot in the shared PGPROC array, publishing its PID and database OID so other backends can find it, initializing per-backend entries in the shared invalidation array and the signal array. Only after registration is the backend visible to the rest of the cluster.

Phase 3 — Per-session catalog initialization

Section titled “Phase 3 — Per-session catalog initialization”

The backend must open the target database, verify it exists and is accessible, authenticate the connecting user, and then bootstrap its local caches. “Bootstrap” means: warm-up the relation cache (at minimum the entries for system catalogs that every query needs), warm-up the system catalog cache, initialize the portal manager, set the search path and client encoding. This happens inside a startup transaction that is committed once initialization is complete.

The generic loop: send ReadyForQuery to the client, block on a read, dispatch the incoming message to the appropriate handler, wrap execution in a transaction context (start a transaction if not already in one, commit or roll back at the end), go back to the top. Error recovery uses a setjmp/longjmp jump point installed once before the loop; any ereport(ERROR) unwinds back to that point, aborts the current transaction, and resumes at the top of the loop.

Most systems distinguish at least two exit paths:

  • Graceful shutdown — finish current command, commit or roll back, run cleanup callbacks, exit with code 0.
  • Crash exit — postmaster detects shared-memory corruption or sends SIGQUIT; the backend skips callbacks that might touch corrupted state and calls _exit() directly.
Generic conceptPostgreSQL name
Process birth entry pointpostmaster_child_launchPostgresMain
PGPROC slot allocationInitProcess (before PostgresMain)
Shared-memory registrationInitProcessPhase2 (inside InitPostgres)
Per-session catalog bootstrapInitPostgres in postinit.c
Relation cache warm-upRelationCacheInitializePhase2/3
System catalog cacheInitCatalogCache
Error-recovery jump pointsigsetjmp(local_sigjmp_buf, 1) in PostgresMain
Command readReadCommandSocketBackend / InteractiveBackend
Simple query dispatchexec_simple_query
Extended query dispatchexec_parse_message / exec_bind_message / exec_execute_message
Transaction wrappingstart_xact_command / finish_xact_command
Graceful shutdowndie handler → proc_exit callbacks
Crash exitquickdie_exit(2)

Every child process in a PostgreSQL cluster has a MyBackendType variable (type BackendType, declared in src/include/miscadmin.h) that identifies its role. The full enum as of REL_18_STABLE:

// BackendType — src/include/miscadmin.h
typedef enum BackendType
{
B_INVALID = 0,
/* Backends and other backend-like processes */
B_BACKEND, /* regular client backend */
B_DEAD_END_BACKEND, /* rejected before auth, drains the connection */
B_AUTOVAC_LAUNCHER,
B_AUTOVAC_WORKER,
B_BG_WORKER,
B_WAL_SENDER,
B_SLOTSYNC_WORKER,
B_STANDALONE_BACKEND, /* postgres -s / single-user mode */
/* Auxiliary processes (no database binding, no heavyweight locks) */
B_ARCHIVER,
B_BG_WRITER,
B_CHECKPOINTER,
B_IO_WORKER, /* PG18 async I/O worker */
B_STARTUP,
B_WAL_RECEIVER,
B_WAL_SUMMARIZER, /* PG18 WAL summarization for incremental backup */
B_WAL_WRITER,
B_LOGGER, /* not connected to shared memory */
} BackendType;

AmRegularBackendProcess() is the macro that tests MyBackendType == B_BACKEND. The lifecycle described in this document is the path followed by B_BACKEND, B_WAL_SENDER, B_BG_WORKER, and B_AUTOVAC_WORKER — the “backend-like” group that calls PostgresMain. Auxiliary processes (B_CHECKPOINTER, B_WAL_WRITER, etc.) have their own entry points and are documented in postgres-aux-processes.md.

Phase 1: postmaster_child_launch → PostgresMain

Section titled “Phase 1: postmaster_child_launch → PostgresMain”

The postmaster calls postmaster_child_launch (in launch_backend.c) to create a new child process. On Unix, this is a fork() that immediately returns the child PID to the postmaster and the child PID 0 to the child. On Windows, EXEC_BACKEND is defined and internal_forkexec is used instead; the child re-reads serialized BackendParameters from a shared-memory segment rather than inheriting them from fork().

The child’s first user-land call is PostgresMain(dbname, username) (for backends spawned under a postmaster) or PostgresSingleUserMain (for the postgres -s standalone path, which then delegates to PostgresMain).

PostgresMain begins with signal handler installation:

// PostgresMain — src/backend/tcop/postgres.c
pqsignal(SIGHUP, SignalHandlerForConfigReload);
pqsignal(SIGINT, StatementCancelHandler); /* cancel current query */
pqsignal(SIGTERM, die); /* graceful shutdown */
pqsignal(SIGQUIT, quickdie); /* hard crash exit */
InitializeTimeouts(); /* installs SIGALRM handler */
// ... condensed ...
pqsignal(SIGUSR1, procsignal_sigusr1_handler);

The signal set here is different from the postmaster’s. SIGTERM maps to die (set ProcDiePending, return to command loop), not to an immediate exit. SIGQUIT maps to quickdie, which calls _exit(2) after a brief attempt to notify the client — bypassing all cleanup callbacks, because shared memory may be corrupt.

Phase 2: BaseInit — local subsystem registration

Section titled “Phase 2: BaseInit — local subsystem registration”

After signal setup, PostgresMain calls BaseInit:

// BaseInit — src/backend/utils/init/postinit.c
BaseInit(void)
{
Assert(MyProc != NULL); /* InitProcess() already ran */
DebugFileOpen();
InitFileAccess();
pgstat_initialize(); /* cumulative stats, early so shutdown hooks run last */
pgaio_init_backend(); /* PG18: async I/O per-backend state */
InitSync();
smgrinit();
InitBufferManagerAccess(); /* per-backend local buffer structures */
InitTemporaryFileAccess();
InitXLogInsert(); /* WAL record construction buffers */
InitLockManagerAccess(); /* local lock manager structures */
ReplicationSlotInitialize();
}

BaseInit initializes the local (per-process) side of shared subsystems. InitBufferManagerAccess allocates the per-backend pin-count array (PrivateRefCountArray). InitLockManagerAccess allocates the local lock hash. None of these calls touch the shared-memory structures yet; that happens in InitPostgres.

Phase 3: InitPostgres — shared registration and catalog bootstrap

Section titled “Phase 3: InitPostgres — shared registration and catalog bootstrap”

InitPostgres is the central initialization function. It is long (≈525 lines in postinit.c) and intentionally sequential: the call order is load-bearing, as the comment at line 710 warns (“Be very careful with the order of calls in the InitPostgres function”).

The sequence, stripped to its skeleton:

// InitPostgres — src/backend/utils/init/postinit.c
void
InitPostgres(const char *in_dbname, Oid dboid,
const char *username, Oid useroid,
bits32 flags, char *out_dbname)
{
/* 1. Register in ProcArray — now visible to other backends */
InitProcessPhase2();
/* 2. Backend-status array entry (for pg_stat_activity) */
pgstat_beinit();
pgstat_bestart_initial();
/* 3. Shared invalidation array entry */
SharedInvalBackendInit(false);
/* 4. ProcSignal slot (for cancel key, SIGUSR1 routing) */
ProcSignalInit(MyCancelKey, MyCancelKeyLength);
/* 5. Register timeout handlers */
RegisterTimeout(DEADLOCK_TIMEOUT, CheckDeadLockAlert);
RegisterTimeout(STATEMENT_TIMEOUT, StatementTimeoutHandler);
// ... condensed ...
/* 6. Initialize local catalog scaffolding (no catalog reads yet) */
RelationCacheInitialize();
InitCatalogCache();
InitPlanCache();
EnablePortalManager();
/* 7. Load shared catalog relcache entries (pg_database, pg_authid, …) */
RelationCacheInitializePhase2();
/* 8. Register shutdown callback */
before_shmem_exit(ShutdownPostgres, 0);
/* 9. Open startup transaction, authenticate, check connection limits */
StartTransactionCommand();
PerformAuthentication(MyProcPort); /* only for regular backends */
InitializeSessionUserId(username, useroid, false);
// ... condensed: connection limit checks, walsender short-circuit ...
/* 10. Lock database OID, set MyDatabaseId, validate directory */
LockSharedObject(DatabaseRelationId, dboid, 0, RowExclusiveLock);
MyDatabaseId = dboid;
MyProc->databaseId = MyDatabaseId;
/* 11. Full relcache warm-up with real database open */
RelationCacheInitializePhase3();
initialize_acl();
CheckMyDatabase(dbname, am_superuser, ...);
/* 12. Startup options and per-role/db GUC settings */
process_startup_options(MyProcPort, am_superuser);
process_settings(MyDatabaseId, GetSessionUserId());
/* 13. Finalize: search path, client encoding, session state, preload libs */
InitializeSearchPath();
InitializeClientEncoding();
InitializeSession();
process_session_preload_libraries(); /* if INIT_PG_LOAD_SESSION_LIBS */
/* 14. Complete pg_stat_activity entry, commit startup transaction */
pgstat_bestart_final();
CommitTransactionCommand();
}

Step 10 is notable: LockSharedObject(DatabaseRelationId, dboid, 0, RowExclusiveLock) takes a brief writer’s lock on the database OID to serialize against a concurrent DROP DATABASE. The lock is held only until the end of the startup transaction (step 14), but by then MyProc->databaseId is set and any DROP DATABASE attempting to remove this database will find this backend in the procarray and wait.

Step 3 (SharedInvalBackendInit) is the entry point into the shared-invalidation ring. Once this is called, the backend will receive cache-invalidation messages from other backends that modify system catalogs, keeping its local relcache and catcache coherent.

Figure 1 — Backend initialization sequence: from fork to ReadyForQuery

sequenceDiagram
    participant PM as postmaster
    participant BE as backend (new)
    participant SHM as shared memory

    PM->>BE: fork() / EXEC_BACKEND
    BE->>BE: install signal handlers (PostgresMain)
    BE->>BE: InitProcess() — allocate PGPROC slot
    BE->>BE: BaseInit() — local subsystem init
    BE->>SHM: InitProcessPhase2() — join ProcArray
    BE->>SHM: SharedInvalBackendInit() — join sinval ring
    BE->>BE: RelationCacheInitialize() — empty hash tables
    BE->>SHM: RelationCacheInitializePhase2() — shared catalog entries
    BE->>BE: StartTransactionCommand() — startup txn
    BE->>SHM: LockSharedObject(DatabaseOID) — lock against DROP DATABASE
    BE->>BE: PerformAuthentication()
    BE->>SHM: MyDatabaseId = dboid; MyProc->databaseId set
    BE->>BE: RelationCacheInitializePhase3() — full relcache
    BE->>BE: CheckMyDatabase(), process_settings()
    BE->>BE: CommitTransactionCommand() — release startup txn + db lock
    BE->>PM: ReadyForQuery sent to client

Figure 1 — The sequential phases of backend startup. Steps up to InitProcessPhase2 make the backend visible in shared memory; LockSharedObject serializes against DROP DATABASE; the startup transaction covers steps 9–14.

After InitPostgres returns, PostgresMain frees PostmasterContext (GUC strings inherited from the postmaster are no longer needed), transitions to NormalProcessing mode, creates MessageContext for per-command allocations, fires any login event triggers, and installs the error-recovery jump point:

// PostgresMain (command loop setup) — src/backend/tcop/postgres.c
if (sigsetjmp(local_sigjmp_buf, 1) != 0)
{
/* arrived here from an ereport(ERROR) or signal */
error_context_stack = NULL;
HOLD_INTERRUPTS();
disable_all_timeouts(false);
QueryCancelPending = false;
pq_comm_reset();
EmitErrorReport();
AbortCurrentTransaction();
MemoryContextSwitchTo(MessageContext);
FlushErrorState();
xact_started = false;
/* resume at top of loop */
}
PG_exception_stack = &local_sigjmp_buf;

This is the outermost setjmp in the backend. It is the only active exception handler during error recovery itself; this is intentional — having no outer PG_TRY means a error during error recovery does not recurse indefinitely but instead immediately re-enters this same handler, which will eventually exhaust elog’s internal stack and terminate cleanly.

The for(;;) loop that follows:

// PostgresMain command loop — src/backend/tcop/postgres.c
for (;;)
{
MemoryContextSwitchTo(MessageContext);
MemoryContextReset(MessageContext); /* free prior command's allocations */
InvalidateCatalogSnapshotConditionally();
if (send_ready_for_query)
{
/* report stats, flush notifies, set ps_display, start idle timers */
pgstat_report_stat(false);
ReadyForQuery(whereToSendOutput); /* sends 'Z' message to client */
send_ready_for_query = false;
}
DoingCommandRead = true;
firstchar = ReadCommand(&input_message); /* blocks here */
DoingCommandRead = false;
CHECK_FOR_INTERRUPTS();
/* reload config if SIGHUP arrived */
if (ConfigReloadPending) ProcessConfigFile(PGC_SIGHUP);
switch (firstchar)
{
case PqMsg_Query: exec_simple_query(query_string); break;
case PqMsg_Parse: exec_parse_message(...); break;
case PqMsg_Bind: exec_bind_message(...); break;
case PqMsg_Execute: exec_execute_message(...); break;
case PqMsg_Describe: exec_describe_statement_message / exec_describe_portal_message; break;
case PqMsg_Terminate: proc_exit(0); /* clean exit */ break;
case EOF: proc_exit(0); break;
// ... condensed ...
}
send_ready_for_query = true;
}

ReadCommand dispatches to SocketBackend (wire protocol) or InteractiveBackend (standalone mode). SocketBackend reads the one-byte message type, validates it against a known set, then reads the length-prefixed body.

Transaction wrapping: start_xact_command / finish_xact_command

Section titled “Transaction wrapping: start_xact_command / finish_xact_command”

Each command that modifies or reads data is wrapped in a transaction context. exec_simple_query calls start_xact_command before invoking the parser/planner/executor and finish_xact_command after:

// start_xact_command — src/backend/tcop/postgres.c
start_xact_command(void)
{
if (!xact_started)
{
StartTransactionCommand();
xact_started = true;
}
else if (MyXactFlags & XACT_FLAGS_PIPELINING)
BeginImplicitTransactionBlock();
enable_statement_timeout();
/* ... condensed: client-connection-check timeout ... */
}
// finish_xact_command — src/backend/tcop/postgres.c
finish_xact_command(void)
{
disable_statement_timeout();
if (xact_started)
{
CommitTransactionCommand();
xact_started = false;
}
}

The xact_started flag prevents double-starting a transaction when multiple Parse/Bind/Execute messages arrive in a pipeline. CommitTransactionCommand descends into xact.c (documented in postgres-xact.md); here the concern is the lifecycle wrapper, not the transaction internals.

Figure 2 — Command loop: ReadyForQuery → ReadCommand → dispatch → transaction wrap → loop

stateDiagram-v2
    [*] --> Idle : InitPostgres complete
    Idle --> Reading : send ReadyForQuery\nDoingCommandRead=true
    Reading --> Dispatching : ReadCommand returns
    Dispatching --> Executing : start_xact_command
    Executing --> Idle : finish_xact_command\nsend_ready_for_query=true
    Executing --> ErrorRecovery : ereport(ERROR)\nlongjmp to sigsetjmp
    ErrorRecovery --> Idle : AbortCurrentTransaction\nFlushErrorState
    Reading --> Shutdown : PqMsg_Terminate or EOF
    Shutdown --> [*] : proc_exit(0)
    Idle --> CrashExit : SIGQUIT
    CrashExit --> [*] : quickdie _exit(2)

Figure 2 — State machine of the command loop. The sigsetjmp error-recovery arc connects Executing back to Idle without touching the outer loop structure.

Graceful shutdown (die handler, triggered by SIGTERM from the postmaster or by a client PqMsg_Terminate): sets ProcDiePending = true and InterruptPending = true. The next CHECK_FOR_INTERRUPTS() call in the main loop sees ProcDiePending, raises FATAL, which unwinds through elog to proc_exit. proc_exit runs the registered before_shmem_exit and on_proc_exit callbacks in reverse registration order — ShutdownPostgres → resource owner cleanup → relcache shutdown → smgr shutdown — then calls _exit(0).

Crash exit (quickdie, triggered by SIGQUIT): the postmaster sends SIGQUIT to all backends when it detects a sibling crash that may have corrupted shared memory. quickdie signals the client with a brief WARNING (best-effort), then calls _exit(2) without running any callbacks. Using _exit(2) rather than _exit(0) is intentional: if someone sends a manual SIGQUIT to a random backend, the non-zero exit code triggers the postmaster’s crash-restart logic, ensuring shared memory is rebuilt even without a real crash.

// quickdie — src/backend/tcop/postgres.c
quickdie(SIGNAL_ARGS)
{
sigaddset(&BlockSig, SIGQUIT); /* prevent recursive quickdie */
sigprocmask(SIG_SETMASK, &BlockSig, NULL);
HOLD_INTERRUPTS();
/* best-effort: notify client with WARNING_CLIENT_ONLY */
error_context_stack = NULL;
ereport(WARNING_CLIENT_ONLY, ...);
/* DO NOT run proc_exit callbacks — shared memory may be corrupted */
_exit(2);
}
  • postmaster_child_launch (launch_backend.c) — entry point; forks child on Unix, calls internal_forkexec on Windows. Sets MyBackendType.
  • PostgresSingleUserMain (postgres.c) — standalone-mode entry; sets up a minimal environment then delegates to PostgresMain.
  • PostgresMain (postgres.c) — signal setup → BaseInitInitPostgres → command loop. The outermost function for all backend-like processes.
  • BaseInit (postinit.c) — local-side initialization of file, stats, AIO, smgr, buffer, WAL-insert, lock, and replication-slot subsystems.
  • InitPostgres (postinit.c) — 14-step shared-registration and catalog-bootstrap sequence; the most safety-critical initialization path.
  • InitProcess (proc.c) — allocates a PGPROC slot from ProcGlobal before PostgresMain is called; called by the postmaster before fork on older paths and by the child in newer paths.
  • InitProcessPhase2 (proc.c) — links MyProc into ProcArray; the moment the backend becomes visible cluster-wide.
  • SharedInvalBackendInit (sinvaladt.c) — registers backend in the shared-invalidation ring; from this point the backend will receive sinval messages.
  • RelationCacheInitialize / RelationCacheInitializePhase2 / RelationCacheInitializePhase3 (relcache.c) — three-phase relcache warm-up: allocate hash → load shared-catalog entries → load database-local entries.
  • InitCatalogCache / InitPlanCache (catcache.c, plancache.c) — allocate syscache and plan-cache hashtables.
  • PerformAuthentication (postinit.c) — calls ClientAuthentication (in auth.c); the HBA/GSSAPI/SCRAM/md5/trust dispatch point.
  • LockSharedObject (called in InitPostgres) — takes a writer’s lock on the database OID to serialize against DROP DATABASE.
  • ReadCommand (postgres.c) — dispatches to SocketBackend or InteractiveBackend depending on whereToSendOutput.
  • SocketBackend (postgres.c) — reads one-byte message type, validates, reads length-prefixed body into inBuf.
  • exec_simple_query (postgres.c) — simple-query protocol handler; calls start_xact_command, then pg_parse_querypg_analyze_and_rewritepg_plan_queriesPortalRun, then finish_xact_command.
  • exec_parse_message / exec_bind_message / exec_execute_message (postgres.c) — extended-query protocol handlers.
  • start_xact_command / finish_xact_command (postgres.c) — transaction lifecycle wrappers; finish_xact_command calls CommitTransactionCommand in xact.c.
  • ReadyForQuery (tcopprot.h / postgres.c) — sends the Z wire message; flush point between commands.
  • ProcessCatchupInterrupt (sinval.c) — processes shared-invalidation catchup messages that arrived while the backend was busy.
  • die (postgres.c) — SIGTERM handler; sets ProcDiePending, defers to next CHECK_FOR_INTERRUPTS.
  • quickdie (postgres.c) — SIGQUIT handler; _exit(2) without callbacks.
  • ShutdownPostgres (postinit.c) — before_shmem_exit callback; cleans up portals, cursors, and aborts any open transaction before shared subsystems shut down.
  • proc_exit (ipc.c) — runs registered before_shmem_exit then on_proc_exit callbacks in reverse order, then _exit(code).

Position hints (as of 2026-06-05, commit 273fe94)

Section titled “Position hints (as of 2026-06-05, commit 273fe94)”
SymbolFileLine
BackendType enumsrc/include/miscadmin.h337
AmRegularBackendProcess macrosrc/include/miscadmin.h381
postmaster_child_launchsrc/backend/postmaster/launch_backend.c229
PostgresSingleUserMainsrc/backend/tcop/postgres.c4059
PostgresMainsrc/backend/tcop/postgres.c4188
sigsetjmp error-recovery blocksrc/backend/tcop/postgres.c4397
for(;;) command loopsrc/backend/tcop/postgres.c4520
ReadCommandsrc/backend/tcop/postgres.c481
SocketBackendsrc/backend/tcop/postgres.c353
start_xact_commandsrc/backend/tcop/postgres.c2787
finish_xact_commandsrc/backend/tcop/postgres.c2826
quickdiesrc/backend/tcop/postgres.c2930
diesrc/backend/tcop/postgres.c3027
BaseInitsrc/backend/utils/init/postinit.c612
InitPostgressrc/backend/utils/init/postinit.c712
PerformAuthenticationsrc/backend/utils/init/postinit.c194
LockSharedObject (db lock)src/backend/utils/init/postinit.c1058
  • BackendType has 18 members in REL_18_STABLE, including B_IO_WORKER (PG18 async I/O worker) and B_WAL_SUMMARIZER (PG18 WAL summarization). Verified by reading src/include/miscadmin.h lines 337–375 at commit 273fe94. B_IO_WORKER and B_WAL_SUMMARIZER are new in PG18; docs must not assert these for PG17.

  • BaseInit calls pgaio_init_backend() (PG18 async I/O). Verified in postinit.c:639. This call does not exist in PG17; the PG18-only async I/O subsystem is described in postgres-aio.md (planned).

  • InitPostgres takes a RowExclusiveLock on the database OID, not a shared lock. Verified at postinit.c:1058. The comment explains the reasoning: a session is considered a concurrent writer of the database, so a writer’s lock is appropriate. The lock is held only until CommitTransactionCommand at the end of InitPostgres.

  • quickdie calls _exit(2), not _exit(0). Verified at postgres.c:3019. The exit code 2 is intentional: it triggers the postmaster’s crash-restart logic via the “dead man switch” in pmsignal.c, even for a manual SIGQUIT to a single backend.

  • sigsetjmp(local_sigjmp_buf, 1) — the second argument is 1 (save signal mask). Verified at postgres.c:4397. The comment explains: this restores UnBlockSig (the backend’s default signal mask) when longjmp’ing back, ensuring signals are not left blocked after a signal handler unwinds via longjmp.

  • MessageContext is reset (not deleted and recreated) at the top of every command loop iteration. Verified at postgres.c:4542–4545. MemoryContextReset frees all children and resets the context’s own allocations without deallocating the context header itself, which is more efficient than MemoryContextDelete + AllocSetContextCreate each cycle.

  • ReadyForQuery calls pgstat_report_stat (cumulative stats flush) and ProcessNotifyInterrupt (LISTEN/NOTIFY delivery) before sending the Z message. Verified at postgres.c:4610–4641. Stats are not flushed on every command, only at idle points, to avoid reporting uncommitted state to autovacuum.

  1. B_DEAD_END_BACKEND lifecycle. A B_DEAD_END_BACKEND is forked when the postmaster has no free PGPROC slots (connection limit exceeded) or encounters an authentication error it wants to communicate before dying. It is unclear whether BaseInit is called for this type or whether the process simply sends an error message and exits immediately. Investigation path: trace postmaster_child_launch with child_type == B_DEAD_END_BACKEND through launch_backend.c and find the corresponding entry point.

  2. EventTriggerOnLogin (PG16 feature). PostgresMain calls EventTriggerOnLogin() after InitPostgres completes (postgres.c:4373). The interaction between a login-event-trigger failure and the command-loop recovery path (does it prevent ReadyForQuery?) is not analyzed in this document. Investigation path: trace EventTriggerOnLogin in event_trigger.c and look for error handling.

  3. XACT_FLAGS_PIPELINING behavior in start_xact_command. BeginImplicitTransactionBlock is called when pipelining is active and the transaction is already started. The interaction with extended-query Sync messages (which normally terminate a pipeline) and the ignore_till_sync flag deserves a dedicated walkthrough in postgres-portals-prepared.md.

Beyond PostgreSQL — Comparative Designs & Research Frontiers

Section titled “Beyond PostgreSQL — Comparative Designs & Research Frontiers”
  • Thread-per-session (MySQL/InnoDB, SQL Server) — the dominant alternative. Architecture of a Database System (Hellerstein et al. 2007, §2.2) provides a balanced comparison: threads are cheaper to create and share memory more easily, but a bug in one thread’s stack can corrupt another session’s data. PostgreSQL’s process model trades lower density for stronger isolation. The relevant comparison: how much overhead does fork() + shared-memory registration add per new connection versus a thread creation + TLS setup?

  • Connection pooling as the process-model scaling fix — PgBouncer (transaction-mode pooling) and the nascent built-in pool in PG17+ decouple client connections from backend processes, reducing the per-session fork overhead. Understanding PostgresMain’s initialization cost is prerequisite to understanding what the pool defers vs reuses.

  • Oracle’s dedicated-vs-shared server model — Oracle dedicates one server process per client in dedicated mode (same as PostgreSQL’s model) but can multiplex many clients onto fewer server processes in shared-server mode (MTS). PostgreSQL has no equivalent of MTS; pgBouncer fills that role externally.

  • Greenplum / Citus coordinator backends — distributed PostgreSQL forks where PostgresMain has been extended to dispatch parts of the query to remote segments. The lifecycle described here is the base that both extend; their changes are mostly in exec_simple_query and plan-execution, not initialization.

  • B_IO_WORKER (PG18 async I/O) — the new B_IO_WORKER backend type follows a different lifecycle path: it does not call InitPostgres and holds no catalog state. Its lifecycle is documented in the planned postgres-aio.md.

  • None (synthesized directly from source tree at REL_18_STABLE / 273fe94).

Source code paths (REL_18_STABLE / 273fe94)

Section titled “Source code paths (REL_18_STABLE / 273fe94)”
  • src/backend/tcop/postgres.cPostgresMain, command loop, signal handlers, start/finish_xact_command, ReadCommand, SocketBackend
  • src/backend/utils/init/postinit.cBaseInit, InitPostgres, PerformAuthentication, ShutdownPostgres
  • src/backend/utils/init/miscinit.cInitializeSessionUserId, InitializeClientEncoding, InitializeSearchPath
  • src/backend/postmaster/launch_backend.cpostmaster_child_launch, PostmasterChildName, SubPostmasterMain (Windows re-entry)
  • src/include/miscadmin.hBackendType enum, Am*Process macros
  • Architecture of a Database System (Hellerstein, Moody, Doan, Balsa, Hellerstein 2007) §2 “Process Models” — thread vs process model comparison. In KB at knowledge/research/dbms-papers/fntdb07-architecture.md.
  • Database Internals (Petrov 2019) ch. 1 — general database process model context.