pgrust Thread-Per-Connection Model: How It Contrasts with PostgreSQL Process-Per-Connection

pgrust replaces PostgreSQL’s heavyweight process‑per‑connection architecture with a lightweight thread‑per‑connection model using Rust’s std::thread::spawn, eliminating expensive fork operations while maintaining safety through Rust’s ownership rules and thread‑local storage.

The malisper/pgrust repository reimplements PostgreSQL’s server architecture in Rust, switching from operating‑system processes to lightweight threads for each client connection. This architectural shift preserves full wire‑protocol compatibility with PostgreSQL clients while delivering substantial performance improvements through reduced connection overhead and better cache locality.

How PostgreSQL Handles Connections (Process-Per-Connection)

PostgreSQL relies on a process‑per‑connection model managed by the postmaster daemon. When a client connects, the postmaster calls fork() to create a new child process that owns its own address space, file descriptors, and static variables. This design provides strong isolation—each backend process operates independently—but incurs significant overhead. The fork system call copies the entire process memory using copy‑on‑write semantics, and context switches between processes are more expensive than thread switches. Additionally, separate address spaces prevent direct sharing of in‑memory data structures, forcing inter‑process communication (IPC) through pipes or shared memory segments.

The pgrust Thread-Per-Connection Architecture

pgrust inverts this model by using a thread‑per‑connection architecture within a single process. Instead of forking, the server listens on a TCP or Unix‑domain socket and spawns a new Rust thread for each accepted connection using std::thread::spawn. All threads share the same virtual address space, enabling direct access to global caches and data structures. To emulate PostgreSQL’s process‑local static variables, pgrust employs Rust’s thread_local! macros, which provide thread‑local storage while the compiler’s ownership rules guarantee data‑race safety.

Key Architectural Differences

The contrast between these models affects resource usage, performance, and scalability:

  • Creation Cost: PostgreSQL’s fork copies the entire process image and initializes a new address space, while pgrust’s std::thread::spawn allocates only a lightweight stack (default ~8 MiB) and registers the thread with the kernel.

  • Context Switching: Process‑level context switches in PostgreSQL require switching memory maps and flushing translation lookaside buffers (TLB), whereas pgrust’s kernel threads incur lower switching costs within the same address space.

  • Memory Usage: PostgreSQL consumes one full process per client, even with copy‑on‑write optimizations. pgrust shares the heap across all connections, using only separate stacks for each thread, significantly reducing memory pressure.

  • Cache Locality: PostgreSQL’s separate address spaces prevent CPU cache sharing between backends. pgrust’s shared memory model allows threads to access the same global data structures, improving cache reuse and reducing memory bandwidth contention.

  • Inter‑Connection Communication: PostgreSQL requires explicit IPC mechanisms for backends to share data. pgrust threads communicate directly through shared memory (e.g., global caches) without serialization overhead.

Implementation in the pgrust Source Code

The threading model is implemented across the server binary and connection handling libraries:

  • crates/postgres/src/main.rs: Contains the entry point that sets up the TcpListener, accepts incoming connections, and calls std::thread::spawn to delegate each client to a dedicated thread.

  • crates/interfaces/libpq/fe/src/client.rs: Implements PgClientConn, the core connection object that runs inside each spawned thread. This type reproduces libpq’s behavior in a thread‑safe way, handling the startup packet exchange and authentication exactly like the original C implementation.

  • thread_local! Macros: Throughout the codebase, these macros emulate PostgreSQL’s static variables by allocating thread‑local storage, ensuring that global state remains isolated per connection without requiring process boundaries.

Practical Example: Spawning a Connection Thread

The following simplified Rust code illustrates how pgrust accepts a client and hands it off to a dedicated thread. The full implementation resides in crates/postgres/src/main.rs and utilizes PgClientConn from crates/interfaces/libpq/fe/src/client.rs.

use std::net::{TcpListener, TcpStream};
use pgrust::crates::interfaces::libpq::fe::client::PgClientConn;
use pgrust::crates::interfaces::libpq::fe::transport::TcpTransport;

fn main() -> std::io::Result<()> {
    // Bind to localhost:5432 (or a Unix-domain socket)
    let listener = TcpListener::bind("127.0.0.1:5432")?;

    for stream in listener.incoming() {
        let stream = stream?;
        // Spawn a lightweight thread for each connection
        std::thread::spawn(move || {
            let transport = TcpTransport::new(stream);
            // Execute startup/authentication compatible with PostgreSQL wire protocol
            match PgClientConn::connect(
                transport,
                &Default::default(), // startup parameters
                None,                // optional password
            ) {
                Ok(mut conn) => {
                    // Simple query loop mirroring the C libpq execution path
                    while let Ok(_result) = conn.exec("SELECT 1") {
                        // Process results...
                    }
                }
                Err(e) => eprintln!("connection error: {}", e),
            }
        });
    }
    Ok(())
}

Key implementation details demonstrated:

  • std::thread::spawn creates the OS thread that will handle the session lifecycle.
  • PgClientConn::connect performs the startup handshake and authentication using the same protocol as PostgreSQL, ensuring compatibility with existing client drivers.
  • Thread‑local state within PgClientConn maintains session data without cross‑thread pollution.

Performance Impact

According to the project’s README.md, the thread‑per‑connection model yields significant performance gains over PostgreSQL’s traditional architecture:

  • Transaction Workloads: Approximately 50% faster due to reduced connection establishment overhead and faster context switching.
  • Analytic Workloads: Approximately 300× faster attributed to improved cache locality and shared memory access patterns that eliminate IPC bottlenecks.

These metrics demonstrate that eliminating the fork syscall and process isolation boundaries translates directly to higher throughput and lower latency for concurrent database sessions.

Summary

  • pgrust replaces PostgreSQL’s fork‑based process model with Rust’s std::thread::spawn, creating lightweight threads instead of heavyweight processes.
  • All threads share a single address space, enabling direct memory sharing and improved CPU cache utilization compared to PostgreSQL’s isolated processes.
  • Rust’s thread_local! macros and ownership system provide the same logical isolation as PostgreSQL’s process‑local static variables without sacrificing safety.
  • The architecture reduces per‑connection memory overhead from a full process image to an ~8 MiB thread stack plus shared heap.
  • Performance benchmarks show ~50% improvement on transactional workloads and ~300× improvement on analytic workloads compared to PostgreSQL.

Frequently Asked Questions

How does pgrust maintain PostgreSQL compatibility while using threads instead of processes?

pgrust preserves compatibility by implementing the identical PostgreSQL wire protocol and startup sequence found in crates/interfaces/libpq/fe/src/client.rs. The PgClientConn type mirrors the C libpq API and processing logic, ensuring that existing PostgreSQL clients can connect without modification. The thread‑per‑connection change is internal to the server and invisible to the client protocol.

What Rust features enable safe thread-per-connection without data races?

The implementation relies on Rust’s ownership and borrowing rules enforced at compile time. Global mutable state is protected via thread_local! macros, which allocate separate storage per thread, and the type system prevents threads from sharing mutable references unsafely. This combination allows pgrust to replicate PostgreSQL’s process‑local static data patterns while guaranteeing thread safety without runtime overhead.

How does memory usage compare between pgrust threads and PostgreSQL processes?

PostgreSQL creates a full OS process for each connection, requiring copy‑on‑write duplication of the entire memory image and separate page tables. In contrast, pgrust allocates only a thread stack (approximately 8 MiB by default) per connection while sharing the heap and code segments. This reduces memory consumption significantly when scaling to thousands of concurrent connections.

Is the thread-per-connection model suitable for production workloads?

While pgrust demonstrates the viability of the thread‑per‑connection model with substantial performance improvements, production readiness depends on the completeness of the PostgreSQL feature implementation in the repository. The architecture itself—using Rust threads—provides the necessary isolation and safety guarantees for high‑concurrency production use, but operators should verify that specific SQL features and administrative functions required for their workloads are fully implemented in the current codebase.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →