PostgreSQL Connection Management: The Process-per-Client Model Deep Dive
Unlike many modern databases and web servers that utilize lightweight green threads or OS threads (such as MySQL or early implementations of Microsoft SQL Server), PostgreSQL employs a process-per-client architecture.
Every time a client application connects to PostgreSQL, the database spins up a dedicated operating system process called a backend process to manage that client’s connection lifecycle, parse queries, plan execution trees, run joins, and return responses.
While this architectural choice introduces non-trivial memory overhead and makes connection pooling indispensable, it provides exceptional fault isolation and stability.
1. High-Level Architecture: Postmaster and Backend Processes
When PostgreSQL starts, the initial process launched is the Postmaster (also known as the supervisor process).
+-------------------+
| Client App |
+---------+---------+
|
TCP Conn | (e.g. :5432)
v
+-------------------+
| Postmaster Process|
+---------+---------+
|
| fork_process()
v
+---------------------------------------------+
| |
v v
+---------------+ +---------------+
|Backend Process| |Backend Process|
| (Client 1) | | (Client 2) |
+-------+-------+ +-------+-------+
| |
+----------------------+----------------------+
|
v
[ Shared Memory Segment ]
(shmget / shmat / Locks)
- Listening for Connections: The Postmaster binds to a configured network port (default
5432) and listens for incoming TCP socket connection requests.
- Handing Off to a Backend: As soon as a TCP handshake completes, the Postmaster calls
fork() to spawn an independent backend child process.
- Query Execution: The backend process reads queries from the connection socket, optimizes and executes them, and writes results back to the client.
- Lifecycle Termination: When the client issues a
DISCONNECT or drops the socket, the associated backend process cleans up its resources and terminates.
2. PostgreSQL Source Code Walkthrough
PostgreSQL’s open-source C codebase documents this mechanism cleanly within the postmaster/ subsystem.
The Main Event Loop: ServerLoop()
Located in src/backend/postmaster/postmaster.c, the heart of the Postmaster process is the ServerLoop() function. It runs an infinite loop waiting for incoming activity across configured network endpoints:
/* Conceptual excerpt from postmaster.c */
static int
ServerLoop(void)
{
for (;;)
{
/* Poll and wait for read events on connection sockets */
WaitEventSetWait(pm_waitset, -1, &event, 1, PG_WAIT_POSTMASTER);
if (event.events & WL_SOCKET_READABLE)
{
/* Client connection is waiting to be accepted */
Port *port = ConnCreate(listen_fd);
if (port)
{
BackendStartup(port);
}
}
}
}
Child Process Initialization: BackendStartup() & postmaster_child_launch()
Once the connection is accepted into a Port structure, the Postmaster initiates child startup:
BackendStartup(port) performs initial memory accounting and verification.
- It delegates execution to
postmaster_child_launch(B_BACKEND, ...).
- Inside
postmaster_child_launch, PostgreSQL calls fork_process() to perform the actual Unix fork.
/* Abstraction wrapper in fork_process.c */
pid_t
fork_process(void)
{
pid_t pid;
/* Collect system stats, check timers, set up signal masks */
pid = fork();
return pid;
}
Upon returning from fork_process(), the operating system has created a separate process duplicate with its own virtual address space and a distinct Process ID (PID).
3. How the Backend Inherits the Client Socket
If the client initiated its TCP connection to the Postmaster process, how does the child backend process read and write data to that specific socket without crossing process boundaries?
This relies on core POSIX operating system semantics:
- Everything is a File Descriptor: In Unix-like operating systems, network sockets are represented as file descriptors (
int fd).
- FD Inheritance on
fork(): When fork() is executed, the child process receives a duplicate table of the parent’s open file descriptors. The child inherits identical references to the underlying file table entries, including the active client socket.
- Postmaster Hand-off: Once the fork is complete, the Postmaster drops its interest in that specific client socket. The backend process takes exclusive ownership, issuing
read(), write(), and close() system calls directly against the inherited file descriptor.
4. Inter-Process Communication (IPC) and Shared Memory
In a multi-threaded architecture, threads share heap memory, global variables, and address spaces. Because PostgreSQL backend processes are fully isolated:
- Global variables modified in one backend process are invisible to another.
- Backend processes still need to coordinate shared locks, transaction status (
pg_xact), buffer pools (cached disk pages), and write-ahead logs (WAL).
To solve this, PostgreSQL allocates a large segment of Shared Memory during Postmaster initialization using OS-level primitives like shmget / shmat (System V IPC) or mmap with MAP_SHARED (POSIX IPC).
Backend Process 1 Backend Process 2 Backend Process 3
[ Private Heap ] [ Private Heap ] [ Private Heap ]
\ | /
\ | /
v v v
+---------------------------------------------------+
| Shared Memory Segment |
| - Shared Buffer Pool (Data Cache) |
| - WAL Buffers |
| - Lock Tables / Latches / Semaphores |
+---------------------------------------------------+
To view all currently active backend processes and their activity directly from SQL, you can inspect the internal view:
SELECT pid, usename, client_addr, state, query
FROM pg_stat_activity;
Each row returned in pg_stat_activity correlates directly to a running OS backend process.
5. Architectural Trade-offs
| Attribute | Process-per-Client (PostgreSQL) | Thread-per-Client (e.g., MySQL) |
|---|
| Memory Footprint | High (each process has its own page tables, private allocations, stack). | Low (threads share process address space and page tables). |
| Fault Tolerance | Exceptional (a segfault in one process crashes only that client). | Poor (a memory violation crashes the entire server). |
| Connection Scaling | Poor directly (struggles with thousands of raw connections). | High (handles higher connection concurrency natively). |
| IPC Complexity | High (relies on explicitly managed shared memory and semaphores). | Low (can communicate directly via heap memory pointers). |
The Crucial Advantage: Fault Tolerance
The primary reason PostgreSQL adopted and maintained this model is robustness. If a backend process encounters a critical bug, an unhandled signal, or a memory segmentation fault (SIGSEGV) during a complex join, only that single child process terminates.
The Postmaster cleans up after the deceased child, rolls back active transactions via the WAL engine, and preserves uptime for all other connected clients. In contrast, in a naive threaded model, an unhandled crash in any thread terminates the entire shared address space, causing full database downtime.
The Disadvantage: Scalability and Memory Saturation
Spawning a new process requires significant OS resources:
- Memory allocation for execution contexts and private work buffers (
work_mem).
- High context-switching overhead across hundreds of active OS processes.
Allowing thousands of microservices or serverless functions to connect directly to PostgreSQL will quickly lead to memory exhaustion, aggressive swapping, and database unresponsiveness.
6. Mitigating the Overhead: Connection Pooling
Because the process-per-client model cannot efficiently handle unbounded ephemeral connections, deploying a specialized connection pooler in front of PostgreSQL is standard practice in production environments.
+-------------+ Thousands of
| App Servers | Ephemeral Connections +------------+ Small Fixed Pool +------------+
| (Lambdas, | -----------------------> | PgBouncer | -------------------> | PostgreSQL |
| K8s Pods) | +------------+ (e.g., 50-100 PIDs)| Server |
+-------------+ +------------+
Tools like PgBouncer or AWS RDS Proxy sit between client applications and the database server:
- Maintain thousands of idle, cheap client connections at the proxy layer.
- Multiplex those incoming requests onto a small, optimal pool of long-lived backend processes (e.g., 50 to 100 connections).
- Keep the PostgreSQL server within its sweet spot for CPU context-switching and shared buffer contention.
Key Takeaways
- Postmaster Lifecycle: The Postmaster listens on port
5432 and calls fork_process() to generate a child backend process for every accepted connection.
- Socket Ownership: Child backend processes inherit client socket file descriptors directly from the parent via POSIX
fork() mechanics.
- Shared Memory IPC: Process memory is isolated; shared data (buffer pool, locks) is coordinated through explicit shared memory segments (
shmget/shmat).
- Reliability vs. Overhead: While this model guarantees that a crash in one client never brings down the rest of the database, it demands connection poolers like PgBouncer to manage memory and avoid process proliferation at scale.