The idea: one process per connection
PostgreSQL does not use threads. When it starts, one process, the postmaster, opens port 5432 and starts a few background processes. Every client connection then gets its own operating-system process, a backend, made with fork(). The processes never call each other. They cooperate through one block of shared memory that every one of them maps, and each backend keeps its own private memory for sorts and hashes.
The canvas draws a toy server with max_connections = 4 (the default is 100), 4 of the 16384 shared buffers, one table users with two 8 KB pages, and five clients C1 to C5.
From connect to ReadyForQuery
- The client opens a TCP connection to port 5432. Only the postmaster listens there.
- The postmaster
accept()s it and forks right away. The child inherits the socket; the postmaster goes back to listening. It never parses client data itself, so a bad packet cannot take down the supervisor. - The child reads the StartupMessage (protocol version, user, database).
InitProcess()takes a free PGPROC entry in shared memory. There are exactlymax_connectionsof them for clients (plus separate ones for autovacuum workers and background processes). If none is free, the child fails here.- The child checks
pg_hba.confand authenticates, usually with SCRAM-SHA-256:AuthenticationSASL, the client's proof,AuthenticationOk. - It sends
ReadyForQuery. From now on the client talks only to this backend, until it disconnects (Demo: a client connects).
A fork costs a few hundred microseconds to a few milliseconds, and the new backend then fills its catalog caches from scratch. That is cheap for a long session and expensive for a web request that connects, runs one query and leaves. Copy-on-write fork shows why the fork itself is not a full memory copy.
Shared and private memory
| Memory | Shared? | What it holds |
|---|---|---|
shared_buffers | shared | 8 KB copies of table and index pages. A page read by one backend is a hit for all others (Demo: two backends share one buffer). |
| WAL buffers | shared | WAL records not yet written to pg_wal. |
| lock table | shared | Table-level locks such as RowExclusiveLock. Row locks live in the tuples themselves. |
| CLOG (pg_xact) | shared cache | Two bits per transaction id: running, committed, aborted. |
| ProcArray | shared | Every process with its running xid; each snapshot reads it. |
| pgstats | shared | Counters such as n_dead_tup that autovacuum reads. |
work_mem | private | Memory for one sort or hash in one backend. A query may use several at once. |
An UPDATE shows how they work together (Demo: UPDATE, WAL and commit): the backend publishes an xid in the ProcArray, takes a lock in the lock table, changes the page in a shared buffer, logs a WAL record in the WAL buffers, and at commit flushes the WAL to pg_wal and marks the xid committed in the CLOG. The data page stays dirty in memory; see the buffer pool and WAL internals for what happens to it next.
Background processes
| Process | Job |
|---|---|
| checkpointer | Every checkpoint_timeout (5 min) or max_wal_size, writes all dirty buffers and logs a checkpoint, the new start point for recovery. |
| background writer | Writes some dirty buffers ahead of time, so backends rarely have to write one to free a buffer. |
| walwriter | Flushes WAL buffers every wal_writer_delay (200 ms), for records no commit is waiting on. |
| autovacuum launcher | Wakes every autovacuum_naptime (1 min), checks n_dead_tup > 50 + 0.2 × reltuples, and asks the postmaster for a worker (Demo: autovacuum launcher starts a worker). |
| autovacuum workers | Up to autovacuum_max_workers (3); each vacuums tables in one database, then exits. |
| walsender | One per standby or logical replication connection; streams WAL. See logical decoding. |
| startup | Only during crash recovery or on a standby: replays WAL. |
Only the postmaster forks. The launcher sets a flag in shared memory and signals the postmaster, which starts the worker. ps shows them all, e.g. postgres: autovacuum worker shop or postgres: app shop 10.0.0.5(51234) idle.
When a backend crashes
If a backend dies from a signal (a segfault, kill -9, the Linux OOM killer), it may have died while holding a spinlock or halfway through changing a shared structure. The postmaster cannot know, so it treats all of shared memory as corrupt:
- It logs
terminating any other active server processesand sendsSIGQUITto every child. Healthy sessions getWARNING: terminating connection because of crash of another server processand the connection is closed. - It deletes and re-creates shared memory. Dirty buffers are lost.
- It starts the startup process, which replays WAL from the last checkpoint's redo point. A record is applied only if the page's LSN is older than the record.
- The checkpointer writes an end-of-recovery checkpoint, the other background processes start again, and the server logs
database system is ready to accept connections.
So one crashing session costs every session its connection, but no committed data (Demo: a backend crashes, crash recovery). restart_after_crash = off makes the postmaster exit instead, for clusters managed by an external failover tool. A plain ERROR or even a FATAL in one session is not a crash: that backend exits cleanly and nobody else notices. Compare ARIES-style recovery, which also has an undo pass; PostgreSQL needs none because aborted rows stay in the heap, invisible.
max_connections and poolers
max_connections is fixed at start, because it sizes the PGPROC array and the lock table in shared memory. When all slots are taken, the next child fails with FATAL: sorry, too many clients already (Demo: too many clients, use a pooler). Three slots are also held back for superusers (superuser_reserved_connections), which the page leaves out.
Raising the limit is not free. Every backend is a process with its own catalog cache and work_mem; every snapshot scans the ProcArray, so a thousand mostly idle connections slow everyone down. The usual answer is a connection pooler such as PgBouncer in transaction mode: thousands of client connections share, say, 20 server connections, and a backend is lent to a client only for one transaction. See Database Connection Pool for how a pool hands out connections, and pool sizing for how many to keep.
What the page leaves out
Superuser-reserved slots, the logger and archiver processes, parallel query workers (also forked by the postmaster; see parallel query), dynamic shared memory, the SLRU caches other than CLOG, the checkpointer's own timing, buffer eviction with the clock sweep, SSL negotiation before the startup packet, query cancellation (a separate connection carrying a secret key), and Windows, where PostgreSQL starts a new process instead of forking. Autovacuum also freezes old tuples and updates the visibility map; see XID wraparound and the free space and visibility maps.