pgrust Thread‑Per‑Connection Model vs PostgreSQL Process‑Per‑Connection Architecture
pgrust replaces PostgreSQL’s heavyweight process‑per‑connection model with lightweight Rust threads, reducing connection overhead by using shared memory instead of forked processes while maintaining full protocol compatibility.
The pgrust project reimplements PostgreSQL’s wire protocol and connection handling in Rust, fundamentally changing how client sessions are managed. Unlike the traditional architecture that spawns a new operating system process for every connection, pgrust adopts a thread‑per‑connection model that leverages Rust’s memory safety guarantees and shared address space. This article explores the technical differences between these architectures by examining the source code in the malisper/pgrust repository.
PostgreSQL's Process‑Per‑Connection Foundation
PostgreSQL’s architecture relies on the postmaster process to fork a new child process for each incoming client connection. According to the source analysis, this design creates a separate operating‑system process that owns its own address space, file descriptors, and static variables.
While this provides strong isolation between connections, it incurs significant costs:
- Creation overhead: Each
forkcopies the whole process memory (using copy‑on‑write), making connection establishment relatively expensive. - Context switching: Process‑level context switches require kernel intervention and are heavier than thread switches.
- Memory duplication: Even with copy‑on‑write optimizations, each connection consumes memory for its own process structures.
- Limited data sharing: In‑memory data structures cannot be shared directly between connections, requiring IPC mechanisms like pipes or shared memory segments.
How pgrust Implements Thread‑Per‑Connection
The pgrust architecture shifts the concurrency model from processes to threads. In crates/postgres/src/main.rs, the server listens on a TCP or Unix‑domain socket and accepts incoming connections. Instead of forking, it spawns a new Rust thread via std::thread::spawn to handle each session.
Key implementation details from the source code:
- Shared address space: All threads run inside a single process, sharing the same virtual address space while maintaining logical isolation through Rust’s ownership rules.
- Thread‑local storage: The code uses
thread_local!macros to emulate PostgreSQL’s process‑local static data, ensuring compatibility with the existing C‑style logic while preventing data races. - Transport abstraction: The
PgClientConntype incrates/interfaces/libpq/fe/src/client.rsimplements the connection logic, using theTransporttrait (defined incrates/interfaces/libpq/fe/src/transport.rs) to abstract over TCP and Unix sockets.
Resource and Performance Comparison
The architectural shift yields measurable differences in resource utilization and throughput:
| Aspect | PostgreSQL (Process‑Per‑Connection) | pgrust (Thread‑Per‑Connection) |
|---|---|---|
| Creation cost | fork + copy‑on‑write of the whole process image |
std::thread::spawn with lightweight stack allocation only |
| Context‑switch cost | Higher (process‑level kernel switches) | Lower (kernel thread switches) |
| Memory usage | One full process per client | One thread stack (~8 MiB default) plus shared heap |
| Cache locality | Separate address spaces limit CPU cache sharing | Shared address space improves cache reuse |
| Communication | Requires IPC (pipes, shared memory) | Direct shared‑memory access for global caches |
| Observed performance | Baseline | ~50 % faster on transaction workloads and ~300× faster on analytic workloads (see README.md line 37) |
Connection Lifecycle in pgrust Source Code
The following simplified example illustrates how pgrust accepts clients and delegates them to dedicated threads. The actual implementation resides in the server binary and the PgClientConn type referenced in crates/interfaces/libpq/fe/src/client.rs (lines 1‑15).
use std::net::{TcpListener, TcpStream};
use pgrust::crates::interfaces::libpq::fe::client::PgClientConn;
use pgrust::crates::interfaces::libpq::fe::transport::TcpTransport;
/// Entry point of the pgrust server (simplified):
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:5432")?;
for stream in listener.incoming() {
let stream = stream?;
// Each connection gets its own thread.
std::thread::spawn(move || {
let transport = TcpTransport::new(stream);
match PgClientConn::connect(
transport,
&Default::default(),
None,
) {
Ok(mut conn) => {
while let Ok(_result) = conn.exec("SELECT 1") {
// Query processing loop
}
}
Err(e) => eprintln!("connection error: {}", e),
}
});
}
Ok(())
}
This pattern demonstrates three critical elements of the thread‑per‑connection model:
TcpListener::acceptprovides the raw socket for incoming connections.std::thread::spawncreates a lightweight OS thread with minimal overhead compared to process forking.PgClientConn::connectexecutes the startup packet exchange and authentication logic, maintaining full compatibility with PostgreSQL’s wire protocol while operating safely within Rust’s thread constraints.
Summary
- pgrust eliminates the
forkoverhead of PostgreSQL by usingstd::thread::spawnto create one thread per client connection instead of one process. - Threads share the same virtual address space, enabling direct access to global caches and reducing memory footprint compared to process‑per‑connection architectures.
- Thread‑local storage in Rust (
thread_local!) safely replicates PostgreSQL’s static process variables without data races. - Performance benchmarks in the repository show significant gains: approximately 50% faster on transaction workloads and roughly 300 times faster on analytic workloads.
- The implementation in
crates/interfaces/libpq/fe/src/client.rsmaintains full protocol compatibility, allowing existing PostgreSQL clients to connect without modification.
Frequently Asked Questions
Is pgrust's thread‑per‑connection model compatible with existing PostgreSQL clients?
Yes. According to the PgClientConn implementation in crates/interfaces/libpq/fe/src/client.rs, pgrust maintains full wire protocol compatibility. The connection handling logic reproduces the startup packet exchange and authentication flow of the original PostgreSQL C code, ensuring that standard libpq clients can connect seamlessly.
How does Rust's ownership model replace PostgreSQL's process isolation?
While PostgreSQL relies on separate address spaces for isolation, pgrust uses Rust’s compile‑time ownership rules and thread_local! macros to isolate connection state. As implemented in the source, this guarantees that data races are impossible without incurring the memory and context‑switch costs of process boundaries.
What are the specific performance differences between pgrust threads and PostgreSQL processes?
The repository’s README.md (line 37) documents that pgrust achieves approximately 50% better throughput on transaction processing workloads and roughly 300× faster execution on analytic workloads compared to the process‑based architecture, primarily due to eliminated fork overhead and improved cache locality.
Can pgrust scale to thousands of concurrent connections like PostgreSQL?
Yes. The lightweight nature of std::thread::spawn allows pgrust to scale to thousands of concurrent sessions without exhausting operating system process limits or file descriptor tables. The shared memory model further reduces resource consumption per connection compared to PostgreSQL’s process‑based approach.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →