PostgreSQL uses one OS process per connection — no thread pools, no coroutines, just
fork().
PostgreSQL follows a classic multi-process architecture. A single long-lived daemon called the postmaster listens for incoming connections and spawns a new child process for each one. These child processes — called backends — handle the full lifecycle of a client session: authentication, SQL parsing, planning, execution, and result delivery. When the client disconnects, the backend exits.
In addition to client backends, the postmaster also launches several auxiliary processes that perform essential housekeeping: flushing dirty buffers to disk (bgwriter, checkpointer), writing WAL (walwriter), archiving WAL segments (archiver), and cleaning up dead tuples (autovacuum launcher and workers). All of these processes share a single region of shared memory, but the postmaster itself deliberately avoids touching that shared memory so it can survive backend crashes and orchestrate recovery.
The process-per-connection model has a clear trade-off. It provides robust isolation — a crash in one backend does not bring down others — at the cost of higher memory usage and connection overhead compared to threaded architectures. This is why connection poolers like PgBouncer are common in production deployments with hundreds of concurrent clients.
| File | Purpose |
|---|---|
src/backend/postmaster/postmaster.c |
Postmaster main loop: listens, accepts, forks |
src/backend/postmaster/launch_backend.c |
postmaster_child_launch() — fork + process setup |
src/backend/postmaster/fork_process.c |
Thin wrapper around fork() |
src/backend/postmaster/pmchild.c |
PMChild slot management (assign, release, find by PID) |
src/backend/tcop/backend_startup.c |
Backend initialization after fork |
src/backend/tcop/postgres.c |
Backend main loop (the “traffic cop”) |
src/include/postmaster/postmaster.h |
PMChild struct, postmaster exports |
src/include/libpq/libpq-be.h |
Port struct (per-connection state) |
src/include/storage/proc.h |
PGPROC struct (per-backend shared memory slot) |
src/include/miscadmin.h |
BackendType enum |
sequenceDiagram
participant C as Client
participant PM as Postmaster
participant BE as Backend
C->>PM: TCP connect (port 5432)
PM->>PM: accept() on listening socket
PM->>PM: AssignPostmasterChildSlot()
PM->>BE: fork() via postmaster_child_launch()
BE->>BE: ClosePostmasterPorts()
BE->>BE: BackendInitialize() --- auth, set up Port
BE->>C: AuthenticationOk
loop Query Loop
C->>BE: Query message ("SELECT ...")
BE->>BE: Parse, Analyze, Rewrite, Plan, Execute
BE->>C: RowDescription + DataRow + CommandComplete
end
C->>BE: Terminate
BE->>BE: proc_exit(0)
PM->>PM: waitpid() reaps child, ReleasePostmasterChildSlot()
select() (or poll()) on its listening sockets inside
ServerLoop() in postmaster.c.accept(), allocates a PMChild slot via
AssignPostmasterChildSlot(), and calls postmaster_child_launch() which
invokes fork().ClosePostmasterPorts()), initializes its Port struct, performs
authentication, and enters the main query loop in PostgresMain().waitpid() in its
signal handler and releases the child’s PMChild slot.The postmaster tracks the cluster’s overall state using a state machine defined in
postmaster.c. Key states include:
When a backend crashes, the postmaster transitions to a recovery state: it sends
SIGQUIT to all surviving children, waits for them to exit, resets shared memory
by launching a new startup process, and returns to PM_RUN.
PostgreSQL classifies child processes using the BackendType enum (defined in
src/include/miscadmin.h). The postmaster uses this to track which processes are
running and apply type-specific policies (for example, autovacuum workers do not
count against max_connections).
| BackendType | Description |
|---|---|
B_BACKEND |
Regular client backend (or walsender before relabeling) |
B_AUTOVAC_LAUNCHER |
Autovacuum launcher (singleton) |
B_AUTOVAC_WORKER |
Autovacuum worker |
B_BG_WORKER |
Background worker (registered via RegisterBackgroundWorker) |
B_BG_WRITER |
Background writer |
B_CHECKPOINTER |
Checkpointer |
B_STARTUP |
Startup process (WAL recovery) |
B_WAL_RECEIVER |
WAL receiver (standby replication) |
B_WAL_SENDER |
WAL sender (primary replication) |
B_WAL_WRITER |
WAL writer |
B_ARCHIVER |
WAL archiver |
postmaster_child_launch() in launch_backend.c calls fork_process(), which is
a thin wrapper around fork(). After the fork:
PMChild struct and
continues accepting connections.PGPROC entry in shared
memory, and enters its type-specific main function.On Windows (EXEC_BACKEND mode), fork() is not available so PostgreSQL uses
CreateProcess() + exec(). The child must re-attach to shared memory and
re-initialize global state, which is why EXEC_BACKEND mode is also used for testing
on Unix.
src/include/postmaster/postmaster.h:40)The postmaster’s record for each child process.
typedef struct
{
pid_t pid; /* OS process ID of the child */
int child_slot; /* index into PMChildFlags array (0 = dead-end child) */
BackendType bkend_type; /* B_BACKEND, B_AUTOVAC_WORKER, etc. */
struct RegisteredBgWorker *rw; /* bgworker info, if applicable */
bool bgworker_notify; /* receives bgworker start/stop notifications */
dlist_node elem; /* link in ActiveChildList */
} PMChild;
child_slot is used as an index into the PMChildFlags[] array in shared memory,
which allows backends to advertise their state to the postmaster without signals.child_slot == 0.src/include/libpq/libpq-be.h:128)Per-connection state, available in every backend as the global MyProcPort.
typedef struct Port
{
pgsocket sock; /* client socket file descriptor */
bool noblock; /* is the socket in non-blocking mode? */
ProtocolVersion proto; /* FE/BE protocol version */
SockAddr laddr; /* local address (server side) */
SockAddr raddr; /* remote address (client side) */
char *remote_host; /* client hostname or IP */
char *remote_hostname; /* resolved hostname, if available */
char *remote_port; /* client port as text */
char *database_name; /* from startup packet */
char *user_name; /* from startup packet */
char *cmdline_options; /* from startup packet */
List *guc_options; /* GUC overrides from startup packet */
HbaLine *hba; /* matched pg_hba.conf line */
/* ... SSL, GSSAPI, keepalive fields omitted for brevity ... */
} Port;
src/include/storage/proc.h:185)Each backend’s slot in the shared ProcArray. This is how backends discover each
other’s transaction state for MVCC visibility checks.
struct PGPROC
{
dlist_node links; /* list link (free list or lock wait queue) */
PGSemaphore sem; /* semaphore for sleeping on */
ProcWaitStatus waitStatus; /* OK, WAITING, or ERROR */
Latch procLatch; /* generic latch for wakeups */
TransactionId xid; /* current top-level transaction ID */
TransactionId xmin; /* horizon: oldest xid this backend cares about */
int pid; /* OS process ID (0 for prepared xacts) */
int pgxactoff; /* offset into ProcGlobal mirrored arrays */
/* ... many more fields ... */
};
xid and xmin are mirrored into dense arrays in ProcGlobal for fast
snapshot computation. Writing these fields requires holding ProcArrayLock or
XidGenLock.procLatch is used extensively for inter-process signaling without Unix signals.graph TD
PM["postmaster (PID 1000)"]
STARTUP["startup (WAL recovery)"]
BG["bgwriter"]
CP["checkpointer"]
WW["walwriter"]
AL["autovacuum launcher"]
AW1["autovacuum worker"]
AR["archiver"]
BE1["backend (user: alice, db: app)"]
BE2["backend (user: bob, db: app)"]
WS["walsender (replica-1)"]
PM --> STARTUP
PM --> BG
PM --> CP
PM --> WW
PM --> AL
AL -.->|"requests via postmaster"| AW1
PM --> AW1
PM --> AR
PM --> BE1
PM --> BE2
PM --> WS
All processes are direct children of the postmaster. The autovacuum launcher requests
new workers by signaling the postmaster (via the AutoVacuumShmem structure in shared
memory), but the postmaster is the one that actually forks them.
Depends on:
fork() / process managementUsed by:
See also: