PostgreSQL Internals Tour

Topics Covered

The Shape of the Server

Why a process rather than a thread per connection

What that isolation buys

What a connection costs

Why connection pooling is not optional

Why the contention rather than the memory sets that ceiling

Why the pool should be smaller than people expect

What the supervising process does

What the background processes are for

Why the background processes are worth knowing by name

Why that visibility is operationally helpful

What lives in shared memory

Why the buffer pool is sized conservatively

Why that double buffering is tolerated

How pages are chosen for eviction

Why large sequential scans use a small ring instead

Why that protection matters in practice

What a backend keeps privately

Why that multiplication is the classic memory mistake

Why plan caches live per backend as well

Why catalogue caches matter more than they sound

What invalidation of those caches costs

Why extensions can replace major components

Why that extensibility shaped the ecosystem

Why the process model is the right place to start

What to take from the server's shape

Every mechanism covered so far appears somewhere in PostgreSQL, and the value of looking at one real engine closely is seeing which choices it made, what those choices cost, and which of its characteristic problems follow directly from them.

Why a process rather than a thread per connection

Each client connection is served by its own operating system process, a decision made when thread support was poor and retained because it gives strong isolation between sessions.

Starting with the process model is right because it explains more operational behaviour than any other single choice.

What that isolation buys

A session that crashes takes its own process down and the server survives, since no other session's memory is in the same address space.

Giving the benefit first is fair. The design has a real virtue.

What a connection costs

Creating a process, attaching it to shared memory and initialising its caches is far more expensive than accepting a socket, and each process holds memory of its own thereafter.

Quantifying the cost is necessary. The expense is the defining consequence.

One process per connection plus a fixed set of background workers, all attached to one shared memory region. The isolation is real and so is the per-connection cost.

Why connection pooling is not optional

An application opening a connection per request pays that cost on every request, so a pool holding a small number of long-lived connections is a requirement rather than an optimisation.

Giving the practical conclusion immediately is right. This is the single most common deployment mistake.

Why the contention rather than the memory sets that ceiling

The backends compete for the same buffer pool locks and the same lock tables, so past a point each additional one slows every other one down rather than adding capacity of its own.

Naming the mechanism is worth doing because the sizing advice sounds arbitrary without it. Queueing a request in a pooler is cheaper than letting it contend inside the server, which is why fewer connections reliably serve more requests.

Why the pool should be smaller than people expect

Each backend consumes memory and competes for the same shared structures, so throughput usually peaks at a connection count near the core count and declines beyond it.

Explaining the sizing is valuable. More connections reduce capacity past a point.

What the supervising process does

It listens for connections, forks a backend for each, starts and restarts the background processes, and does no query work itself.

Describing the supervisor is necessary. One process owns the lifecycle.

What the background processes are for

Writing dirty pages, performing checkpoints, flushing the log, archiving it, running vacuum, and collecting statistics, each as a separate process with one job.

Listing them is useful. The division of labour is visible in any process listing.

Why the background processes are worth knowing by name

Each one corresponds to a mechanism from the earlier lessons, so recognising which process is busy tells you which mechanism is under strain without any further investigation.

Making the mapping explicit is useful. A checkpointer consuming storage bandwidth, a vacuum worker running continuously and a log writer saturating a device are three different diagnoses, and the process name gives each of them away immediately.

Why that visibility is operationally helpful

A problem usually shows as one named process consuming resources, which localises the diagnosis in a way a thread pool does not.

Giving the practical benefit is worthwhile. The model is legible.

What lives in shared memory

The buffer pool, the lock tables, the transaction status structures and the log buffers, all of which every backend attaches to and coordinates through.

Describing the shared region is necessary. The processes are separate and the data is not.

Why the buffer pool is sized conservatively

PostgreSQL reads through the operating system's page cache, so pages are frequently held twice, and giving the engine too much leaves the system cache too little.

Covering the double-buffering reality is important. The sizing rule follows from it.

Why that double buffering is tolerated

Relying on the operating system for part of the caching simplifies the engine considerably, and the duplication costs memory rather than correctness.

Explaining the trade is fair. The choice is deliberate.

How pages are chosen for eviction

A sweep walks the buffers decrementing a usage count and evicts the first one at zero, which approximates recency cheaply without maintaining an ordered list.

Covering the replacement policy is necessary. The approximation is the standard one.

Why large sequential scans use a small ring instead

A scan reading a table larger than the pool would otherwise evict everything useful, so it is confined to a small set of buffers it reuses.

Covering the protection is valuable. The engine defends its cache from one access pattern.

Why that protection matters in practice

Without it a single reporting query would destroy the cache for every transactional query, which is exactly the interference operational systems cannot tolerate.

Explaining the consequence is right. The mechanism prevents a real failure.

What a backend keeps privately

Its own memory for sorting and hashing, its own catalogue caches, and its own plan caches, all of which are per-process and therefore multiplied by the connection count.

Covering private memory is necessary. The multiplication is the sizing trap.

Why that multiplication is the classic memory mistake

A working-memory setting that looks modest is consumed once per sorting operator per connection, so a generous value with many connections can exhaust the machine.

Giving the trap explicitly is practical. The setting is per operation rather than per server.

Why plan caches live per backend as well

A prepared statement's plan is held by the backend that prepared it, so the same statement is planned once per connection rather than once per server.

Covering the per-backend plan cache is worth a line because it explains a real surprise. A pool that recycles connections aggressively loses those plans repeatedly, and a pool that holds them keeps plans chosen from parameters that may no longer be representative.

Why catalogue caches matter more than they sound

Every query must resolve names to object identifiers, and doing that from disk each time would dominate short queries, so each backend caches the catalogue aggressively.

Covering the caches is worthwhile. The optimisation is invisible until it misbehaves.

What invalidation of those caches costs

A schema change must be communicated to every backend, which is why altering a heavily used table briefly disturbs all sessions rather than only the one changing it.

Explaining the consequence is valuable. The cost is cluster-wide and brief.

Why extensions can replace major components

The engine exposes interfaces for index types, access methods, planner hooks and background workers, so substantial functionality can be added without modifying the server.

Covering extensibility is necessary. The architecture's openness is one of its defining features.

Why that extensibility shaped the ecosystem

Capabilities that other engines ship in the core arrive here as extensions, which makes the engine unusually adaptable and makes its deployments unusually varied.

Explaining the consequence is useful. The model has an effect on operations.

Why the process model is the right place to start

Connection cost, memory multiplication and the behaviour of the background workers explain a large share of real incidents, and all three follow from this one decision.

Justifying the section's placement is worthwhile. The architecture predicts the problems.

What to take from the server's shape

Three things. One process per connection gives strong isolation and makes every connection expensive, so pooling is a requirement and the pool should be sized near the core count rather than generously. The buffer pool sits in shared memory above the operating system's page cache, which means pages are often held twice and the pool is therefore sized conservatively, with a sweep over usage counts for eviction and a small ring to stop large scans destroying the cache. And each backend holds private memory for sorting, catalogue caches and plan caches, so a working-memory setting is consumed once per operator per connection, which is the most common way these servers run out of memory.

Common Pitfall

The working-memory setting is the classic PostgreSQL memory mistake, because it is per sorting or hashing operation rather than per server. A value that looks modest is consumed once by every sort, hash join and hash aggregate in every concurrently executing query, so a single query with several such operators can take a multiple of it, and a hundred connections doing the same can exhaust a machine that appeared to have ample headroom. The arithmetic to do is the setting times the operators per query times the concurrency, not the setting alone.