PostgreSQL Backend Lifecycle — Fork, Initialize, Command Loop, and Exit
Contents:
- Theoretical Background
- Common DBMS Design
- PostgreSQL’s Approach
- Source Walkthrough
- Source verification (as of 2026-06-05)
- Beyond PostgreSQL — Comparative Designs & Research Frontiers
- Sources
Theoretical Background
Section titled “Theoretical Background”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:
-
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.
-
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:
- 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.
- 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. - 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.”
Common DBMS Design
Section titled “Common DBMS Design”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.
Phase 2 — Shared-memory registration
Section titled “Phase 2 — Shared-memory registration”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.
Phase 4 — Command loop
Section titled “Phase 4 — Command loop”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.
Shutdown variants
Section titled “Shutdown variants”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.
Theory ↔ PostgreSQL mapping
Section titled “Theory ↔ PostgreSQL mapping”| Generic concept | PostgreSQL name |
|---|---|
| Process birth entry point | postmaster_child_launch → PostgresMain |
| PGPROC slot allocation | InitProcess (before PostgresMain) |
| Shared-memory registration | InitProcessPhase2 (inside InitPostgres) |
| Per-session catalog bootstrap | InitPostgres in postinit.c |
| Relation cache warm-up | RelationCacheInitializePhase2/3 |
| System catalog cache | InitCatalogCache |
| Error-recovery jump point | sigsetjmp(local_sigjmp_buf, 1) in PostgresMain |
| Command read | ReadCommand → SocketBackend / InteractiveBackend |
| Simple query dispatch | exec_simple_query |
| Extended query dispatch | exec_parse_message / exec_bind_message / exec_execute_message |
| Transaction wrapping | start_xact_command / finish_xact_command |
| Graceful shutdown | die handler → proc_exit callbacks |
| Crash exit | quickdie → _exit(2) |
PostgreSQL’s Approach
Section titled “PostgreSQL’s Approach”The BackendType taxonomy
Section titled “The BackendType taxonomy”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.htypedef 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.cpqsignal(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.cBaseInit(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.cvoidInitPostgres(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.
Phase 4: The command loop
Section titled “Phase 4: The command loop”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.cif (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.cfor (;;){ 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.cstart_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.cfinish_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.
Shutdown: graceful vs crash
Section titled “Shutdown: graceful vs crash”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.cquickdie(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);}Source Walkthrough
Section titled “Source Walkthrough”Initialization subsystem
Section titled “Initialization subsystem”postmaster_child_launch(launch_backend.c) — entry point; forks child on Unix, callsinternal_forkexecon Windows. SetsMyBackendType.PostgresSingleUserMain(postgres.c) — standalone-mode entry; sets up a minimal environment then delegates toPostgresMain.PostgresMain(postgres.c) — signal setup →BaseInit→InitPostgres→ 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 aPGPROCslot fromProcGlobalbeforePostgresMainis called; called by the postmaster before fork on older paths and by the child in newer paths.InitProcessPhase2(proc.c) — linksMyProcintoProcArray; 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) — callsClientAuthentication(inauth.c); the HBA/GSSAPI/SCRAM/md5/trust dispatch point.LockSharedObject(called inInitPostgres) — takes a writer’s lock on the database OID to serialize againstDROP DATABASE.
Command-loop subsystem
Section titled “Command-loop subsystem”ReadCommand(postgres.c) — dispatches toSocketBackendorInteractiveBackenddepending onwhereToSendOutput.SocketBackend(postgres.c) — reads one-byte message type, validates, reads length-prefixed body intoinBuf.exec_simple_query(postgres.c) — simple-query protocol handler; callsstart_xact_command, thenpg_parse_query→pg_analyze_and_rewrite→pg_plan_queries→PortalRun, thenfinish_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_commandcallsCommitTransactionCommandinxact.c.ReadyForQuery(tcopprot.h/postgres.c) — sends theZwire message; flush point between commands.ProcessCatchupInterrupt(sinval.c) — processes shared-invalidation catchup messages that arrived while the backend was busy.
Shutdown subsystem
Section titled “Shutdown subsystem”die(postgres.c) —SIGTERMhandler; setsProcDiePending, defers to nextCHECK_FOR_INTERRUPTS.quickdie(postgres.c) —SIGQUIThandler;_exit(2)without callbacks.ShutdownPostgres(postinit.c) —before_shmem_exitcallback; cleans up portals, cursors, and aborts any open transaction before shared subsystems shut down.proc_exit(ipc.c) — runs registeredbefore_shmem_exitthenon_proc_exitcallbacks 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)”| Symbol | File | Line |
|---|---|---|
BackendType enum | src/include/miscadmin.h | 337 |
AmRegularBackendProcess macro | src/include/miscadmin.h | 381 |
postmaster_child_launch | src/backend/postmaster/launch_backend.c | 229 |
PostgresSingleUserMain | src/backend/tcop/postgres.c | 4059 |
PostgresMain | src/backend/tcop/postgres.c | 4188 |
sigsetjmp error-recovery block | src/backend/tcop/postgres.c | 4397 |
for(;;) command loop | src/backend/tcop/postgres.c | 4520 |
ReadCommand | src/backend/tcop/postgres.c | 481 |
SocketBackend | src/backend/tcop/postgres.c | 353 |
start_xact_command | src/backend/tcop/postgres.c | 2787 |
finish_xact_command | src/backend/tcop/postgres.c | 2826 |
quickdie | src/backend/tcop/postgres.c | 2930 |
die | src/backend/tcop/postgres.c | 3027 |
BaseInit | src/backend/utils/init/postinit.c | 612 |
InitPostgres | src/backend/utils/init/postinit.c | 712 |
PerformAuthentication | src/backend/utils/init/postinit.c | 194 |
LockSharedObject (db lock) | src/backend/utils/init/postinit.c | 1058 |
Source verification (as of 2026-06-05)
Section titled “Source verification (as of 2026-06-05)”Verified facts
Section titled “Verified facts”-
BackendTypehas 18 members in REL_18_STABLE, includingB_IO_WORKER(PG18 async I/O worker) andB_WAL_SUMMARIZER(PG18 WAL summarization). Verified by readingsrc/include/miscadmin.hlines 337–375 at commit 273fe94.B_IO_WORKERandB_WAL_SUMMARIZERare new in PG18; docs must not assert these for PG17. -
BaseInitcallspgaio_init_backend()(PG18 async I/O). Verified inpostinit.c:639. This call does not exist in PG17; the PG18-only async I/O subsystem is described inpostgres-aio.md(planned). -
InitPostgrestakes aRowExclusiveLockon the database OID, not a shared lock. Verified atpostinit.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 untilCommitTransactionCommandat the end ofInitPostgres. -
quickdiecalls_exit(2), not_exit(0). Verified atpostgres.c:3019. The exit code 2 is intentional: it triggers the postmaster’s crash-restart logic via the “dead man switch” inpmsignal.c, even for a manualSIGQUITto a single backend. -
sigsetjmp(local_sigjmp_buf, 1)— the second argument is1(save signal mask). Verified atpostgres.c:4397. The comment explains: this restoresUnBlockSig(the backend’s default signal mask) when longjmp’ing back, ensuring signals are not left blocked after a signal handler unwinds via longjmp. -
MessageContextis reset (not deleted and recreated) at the top of every command loop iteration. Verified atpostgres.c:4542–4545.MemoryContextResetfrees all children and resets the context’s own allocations without deallocating the context header itself, which is more efficient thanMemoryContextDelete+AllocSetContextCreateeach cycle. -
ReadyForQuerycallspgstat_report_stat(cumulative stats flush) andProcessNotifyInterrupt(LISTEN/NOTIFY delivery) before sending theZmessage. Verified atpostgres.c:4610–4641. Stats are not flushed on every command, only at idle points, to avoid reporting uncommitted state to autovacuum.
Open questions
Section titled “Open questions”-
B_DEAD_END_BACKENDlifecycle. AB_DEAD_END_BACKENDis forked when the postmaster has no freePGPROCslots (connection limit exceeded) or encounters an authentication error it wants to communicate before dying. It is unclear whetherBaseInitis called for this type or whether the process simply sends an error message and exits immediately. Investigation path: tracepostmaster_child_launchwithchild_type == B_DEAD_END_BACKENDthroughlaunch_backend.cand find the corresponding entry point. -
EventTriggerOnLogin(PG16 feature).PostgresMaincallsEventTriggerOnLogin()afterInitPostgrescompletes (postgres.c:4373). The interaction between a login-event-trigger failure and the command-loop recovery path (does it preventReadyForQuery?) is not analyzed in this document. Investigation path: traceEventTriggerOnLogininevent_trigger.cand look for error handling. -
XACT_FLAGS_PIPELININGbehavior instart_xact_command.BeginImplicitTransactionBlockis called when pipelining is active and the transaction is already started. The interaction with extended-querySyncmessages (which normally terminate a pipeline) and theignore_till_syncflag deserves a dedicated walkthrough inpostgres-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
PostgresMainhas 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 inexec_simple_queryand plan-execution, not initialization. -
B_IO_WORKER(PG18 async I/O) — the newB_IO_WORKERbackend type follows a different lifecycle path: it does not callInitPostgresand holds no catalog state. Its lifecycle is documented in the plannedpostgres-aio.md.
Sources
Section titled “Sources”Raw source files consumed
Section titled “Raw source files consumed”- 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.c—PostgresMain, command loop, signal handlers,start/finish_xact_command,ReadCommand,SocketBackendsrc/backend/utils/init/postinit.c—BaseInit,InitPostgres,PerformAuthentication,ShutdownPostgressrc/backend/utils/init/miscinit.c—InitializeSessionUserId,InitializeClientEncoding,InitializeSearchPathsrc/backend/postmaster/launch_backend.c—postmaster_child_launch,PostmasterChildName,SubPostmasterMain(Windows re-entry)src/include/miscadmin.h—BackendTypeenum,Am*Processmacros
Textbook anchors
Section titled “Textbook anchors”- 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.