DB Internals AtlasPostgreSQL 18.6 Home About

Architecture

Process model

PostgreSQL does not use threads for sessions. The postmaster forks a separate backend process for every connection, and all processes cooperate through shared memory.

In plain words

Every time an application connects, PostgreSQL starts a brand-new program just for that connection: a backend process. The postmaster (the first process, the one listening on the port) does nothing but welcome the connection and fork (clone) a backend for it. From then on the client talks only to its own backend.

Each backend has its own private memory for sorting, hashing and caching table definitions, and all of them share one big area of shared memory with the table pages and the bookkeeping that lets them coordinate.

Starting a process costs a few milliseconds and each one needs some memory even when idle, so thousands of connections are expensive. That is why many sites put a connection pooler such as PgBouncer in front: thousands of app connections share a few dozen real backends.

The flip side is safety: if one backend crashes, the postmaster notices, stops all the others (they might have touched shared memory), runs a quick recovery and starts accepting connections again.

For DBAs and operators

Who forks whom

The postmaster creates shared memory, then starts the startup process (recovery), checkpointer, background writer, WAL writer, autovacuum launcher, logical replication launcher and, since PostgreSQL 18 with the default io_method = worker, the io worker processes. For each new connection it accepts the socket and forks a backend; the child then reads the startup packet, authenticates using pg_hba.conf, attaches to its database and serves the session until disconnect. The postmaster deliberately stays out of shared memory as far as possible, so it survives a backend crash.

Seeing the processes

SELECT pid, backend_type, usename, datname, state, wait_event_type, wait_event
  FROM pg_stat_activity
 ORDER BY backend_type, pid;

In ps the titles look like postgres: app appdb 10.0.0.5(51234) idle, postgres: checkpointer or postgres: io worker 0.

Connection slots

Shared memory holds one PGPROC slot per allowed connection (plus slots for background and autovacuum workers), sized at start-up from max_connections, so changing it requires a restart. The last superuser_reserved_connections (3) slots are kept for superusers, and reserved_connections (since PostgreSQL 16) for roles granted pg_use_reserved_connections. When everything is taken:

FATAL: sorry, too many clients alreadyFATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute

Connection logging in PostgreSQL 18

log_connections is now a list: receipt, authentication, authorization, setup_durations or all (on still means the first three). setup_durations adds a line showing where connection time went:

LOG: connection ready: setup total=8.120 ms, fork=0.512 ms, authentication=4.731 ms

When a backend dies

If a backend exits abnormally (segfault, kill -9, the Linux OOM killer) the postmaster cannot know whether shared memory is consistent, so it terminates every other backend and reinitialises, replaying WAL from the last checkpoint:

LOG: client backend (PID 4521) was terminated by signal 9: KilledLOG: terminating any other active server processesLOG: all server processes terminated; reinitializing

All sessions are dropped, which is why a single OOM-killed query looks like a full outage. Set vm.overcommit_memory = 2 on dedicated hosts, and never kill -9 a backend: use pg_cancel_backend() or pg_terminate_backend().

Why poolers

  • Fork plus authentication typically costs a few milliseconds; TLS and SCRAM add more.
  • Each backend keeps private catalog caches (several MB with many tables) and, without huge pages, its own page-table entries for shared memory.
  • Snapshots scan the proc array, and lock and LWLock contention grows with active sessions. Throughput usually peaks at a small multiple of CPU cores.

Use transaction-mode pooling (PgBouncer, pgcat, Odyssey) to keep active backends close to the core count, and set idle_in_transaction_session_timeout so forgotten transactions do not hold slots and snapshots.

PostgreSQL 18 source trail: postmaster.c documents connection forking, startup/shutdown supervision and backend-crash recovery.

For developers: inside the source

From accept() to a running backend

ServerLoop()                               postmaster/postmaster.c
  AcceptConnection()                       libpq/pqcomm.c
  BackendStartup(): canAcceptConnections(),
                    AssignPostmasterChildSlot(B_BACKEND)
    postmaster_child_launch() → fork_process()
--- child ---
BackendMain()                              tcop/backend_startup.c
  BackendInitialize() → ProcessStartupPacket()
  InitProcess()                            takes a PGPROC
  PostgresMain() → InitPostgres()
    PerformAuthentication() → ClientAuthentication()
  • launch_backend.c: postmaster_child_launch() and the child_process_kinds[] table mapping each BackendType to its main function (BackendMain, CheckpointerMain, AutoVacLauncherMain …). With EXEC_BACKEND (Windows) the child is exec'd and re-attaches to shared memory instead of inheriting it.
  • pmchild.c: the postmaster's own bookkeeping, a PMChild per child (pid, child_slot, bkend_type, rw for bgworkers), kept in private memory so a corrupted shared memory cannot break it.
  • backend_startup.c: startup packet, SSL/GSS negotiation, the 18 log_connections options.
  • proc.c: InitProcGlobal() preallocates the PGPROC array; InitProcess() pops one from ProcGlobal->freeProcs (or the autovacuum, bgworker or walsender list) and raises "sorry, too many clients already" when empty. MaxBackends = max_connections + autovacuum_worker_slots + max_worker_processes + max_wal_senders + special workers (InitializeMaxBackends() in postinit.c, where InitPostgres() also enforces the reserved-slot checks).

Crash handling

process_pm_child_exit() reaps children; an abnormal exit goes to HandleChildCrash() → HandleFatalError(), which sends SIGQUIT to everyone (backends run quickdie() in postgres.c). When all are gone the postmaster logs "all server processes terminated; reinitializing", recreates shared memory and starts a new startup process. restart_after_crash = off makes it exit instead.

Experiment and extend

  • TAP tests: src/test/postmaster/t/002_connection_limits.pl (slot limits), src/test/recovery/t/013_crash_restart.pl (kill a backend, watch the restart).
  • Extensions can add processes with the background worker API (RegisterBackgroundWorker() / RegisterDynamicBackgroundWorker() in bgworker.c) and run code at connection time with ClientAuthentication_hook (see contrib/auth_delay).
  • pg_stat_activity.backend_type comes from GetBackendTypeDesc() in miscinit.c.

Parameters that shape this

max_connections superuser_reserved_connections reserved_connections log_connections log_disconnections idle_session_timeout idle_in_transaction_session_timeout restart_after_crash

Related topics

Server startup sequence Server shutdown modes Postmaster Client backend Memory architecture Local (per-backend) memory Client authentication Wire protocol Crash recovery