Hacktakes · Edition 21
Hacktakes · Edition 21 · August 17, 2026

Why Postgres Uses Processes

The process-per-connection architecture of Postgres is a relic from an era without lightweight threads, not a deliberate fault-tolerance mechanism.

By Leo Marchetti

Sparked by Does anyone run Postgres without PgBouncer? · discussion

Table four had a minor spill, so we must assume the entire dining room is corrupted.
Table four had a minor spill, so we must assume the entire dining room is corrupted.

Read Hacker News long enough, and you'll find out that everyone loves Postgres, but everyone hates managing Postgres connections. It is a compelling, persistent internet myth that brilliant engineers somehow simply forgot to build a modern connection handler. A recent comment called its lack of built-in connection pooling an utterly retrograde design, while Brandur Leach famously called it a car without a windshield. It genuinely feels bugnuts crazy to have to duct-tape PgBouncer to every single production deployment just to achieve basic connection multiplexing.

Before we dig in, I want to reiterate that I am not a database engine developer, so please don't quote me on internal Postgres lock management. I have exactly zero commits in the core tree. But I do know how to read 40-year-old academic papers, and I wanted to solve a mystery: why did they build something that requires us to inject a network proxy into every deployment?

To figure out why, we have to chronologically reverse-engineer Michael Stonebraker's 1986 "The Design of Postgres" paper. When examining historical software architecture, the most useful question a researcher can ask is what specific constraints the original authors were operating under. Today, we assume any high-performance backend system will internally spin up lightweight threads to handle concurrent network requests. Postgres famously relies on a strict process-per-connection model. Every time a new client connects, the operating system forks an entirely separate, heavyweight process.

Modern engineers look at this setup and see an archaic, inefficient bottleneck. Surely Stonebraker and his academic team could have implemented a highly optimized, internal lightweight thread pool to avoid overwhelming the server memory?

POSIX threads were standardized in 1995.

In 1986, standardized lightweight threads literally did not exist. The operating system process stood as the only mature, universally reliable concurrency boundary available on UNIX systems. The database completely delegated all concurrency execution to the host machine's kernel scheduler because replicating a process scheduler inside a user-space application would have been an absurd waste of academic research time.

Many practitioners defend the process-per-connection architecture today by arguing it provides superior crash isolation. The theory dictates that if a rogue query causes a segmentation fault, an isolated OS process dies quietly without tearing down the surrounding database engine. We can test that heuristic empirically by looking at how the primary Postgres master process, the Postmaster, actually handles a backend crash.

/* from src/backend/postmaster/postmaster.c */
if (WIFSIGNALED(exitstatus))
{
    ereport(LOG,
            (errmsg("server process (PID %d) was terminated by signal %d",
                    pid, WTERMSIG(exitstatus))));
    
    /* 
     * We must assume that shared memory has been corrupted.
     * Time to panic and terminate all active connections.
     */
    HandleChildCrash(pid, exitstatus, _("server process"));
}

When a Postgres backend encounters a fatal memory violation, it generates the following log output and immediately tears down the entire system:

LOG: server process (PID 12345) was terminated by signal 11: Segmentation fault
LOG: terminating any other active server processes
FATAL: the database system is in recovery mode

The supposed fault tolerance is an illusion. Because all Postgres backends rely on large blocks of shared memory to communicate, the Postmaster kills all other connections anyway the moment a single backend crashes.[^1] The engine cannot safely assume the shared buffers remain uncorrupted. The architecture isn't a magical fault-tolerance mechanism at all. It's just a direct reflection of the raw operating constraints of 1980s UNIX.

Once we recognize those vintage constraints, the awkwardness of modern cloud deployments makes total sense. We are experiencing a massive shift in sociological expectations regarding what a software layer should actually do. In the 1980s, the dominant philosophy dictated trusting the operating system to manage hardware resources. The kernel scheduled execution; the database stored records.

Today, those boundaries have radically expanded. We expect enterprise databases to behave as entirely self-contained ecosystems that bypass the host kernel entirely. To see what that looks like in practice, consider Microsoft SQL Server. Microsoft refused to trust the host Windows thread scheduler for high-concurrency data workloads, opting instead to build an internal Operating System (OS) like environment called SQLOS. We can visualize this shift in trust:

[ 1986: Trust the OS ]          [ Modern: The DB is the OS ]
+----------------------+        +----------------------+
|   Host Kernel OS     |        |   Host Kernel OS     |
|  (Process Scheduler) |        |    (Bypassed)        |
|    /      |      \   |        +----------------------+
| +----+ +----+ +----+ |        |  Database Engine     |
| | DB | | DB | | DB | |        |  [ Internal SQLOS ]  |
| |Proc| |Proc| |Proc| |        |  (Thread Scheduler)  |
| +----+ +----+ +----+ |        |   /      |      \    |
+----------------------+        | [Th]   [Th]   [Th]   |
                                +----------------------+

<dfn>SQLOS</dfn> provides its own user-mode thread scheduling, memory management, and synchronization primitives running completely independently of the host machine. When you compare Postgres to a modern system running a bespoke internal kernel, the lack of a built-in connection pool naturally feels like a glaring omission.

If you find yourself exhausted by configuring third-party network proxies just to keep your database responsive under load, you shouldn't feel bad. Setting up production infrastructure is uniquely difficult, and it is entirely rational to expect a modern database to handle a thousand concurrent network connections gracefully. The friction is real, stemming purely from fundamental historical constraints that survived into the present day. You are stretching a 1986 trust model into an era of massive, microservice-driven combinatorics.

I could be totally off-base here. Digging through four-decade-old engineering decisions is inherently messy, and maybe there's a nuance in the 1986 BSD process scheduler I completely missed. But the next time you find yourself hair-pullingly frustrated while configuring PgBouncer, don't dropkick the computer. Just remember you're wrestling with the ghost of a time when threads were just a theoretical dream.

[^1]: Core maintainers have explicitly confirmed on the development mailing lists that upon a true segmentation fault, the only safe thing to do is kill all backends.

← Back to Edition 21