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:
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:
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:
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 thechild_process_kinds[]table mapping eachBackendTypeto its main function (BackendMain,CheckpointerMain,AutoVacLauncherMain…). WithEXEC_BACKEND(Windows) the child is exec'd and re-attaches to shared memory instead of inheriting it.pmchild.c: the postmaster's own bookkeeping, aPMChildper child (pid,child_slot,bkend_type,rwfor bgworkers), kept in private memory so a corrupted shared memory cannot break it.backend_startup.c: startup packet, SSL/GSS negotiation, the 18log_connectionsoptions.proc.c:InitProcGlobal()preallocates thePGPROCarray;InitProcess()pops one fromProcGlobal->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()inpostinit.c, whereInitPostgres()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()inbgworker.c) and run code at connection time withClientAuthentication_hook(seecontrib/auth_delay). pg_stat_activity.backend_typecomes fromGetBackendTypeDesc()inmiscinit.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