chopin-pg
High-fidelity engineering for the modern virtuoso.
chopin-pg is a high‑performance, zero‑dependency PostgreSQL driver for the Chopin suite. Built for thread‑per‑core architectures with synchronous non‑blocking I/O, per‑worker connection pools, and zero external runtime dependencies (only libc).
Features
- Zero external dependencies — all crypto (SCRAM-SHA-256), codec, and protocol are hand-written
- Thread-per-core — each worker owns its connections and pool; no
Arc, noMutex - Synchronous non-blocking I/O — sockets in NB mode with poll-based application-level timeouts
- Extended Query Protocol — prepared statements with binary parameter encoding
- Statement cache — FNV-1a hash-based with LRU eviction and configurable capacity
- Connection pool —
PgPoolwith checkout timeout, idle/max lifetime, test-on-checkout, auto-reconnect - COPY protocol — bulk
COPY IN/COPY OUTwith streamingCopyWriter/CopyReader - LISTEN/NOTIFY — async notification support with buffered delivery
- Transactions —
begin/commit/rollback, savepoints, nested transactions, closure-based API - 22 PostgreSQL types — Bool, Int2/4/8, Float4/8, Text, Bytes, Json, Jsonb, Uuid, Date, Time, Timestamp, Timestamptz, Interval, Inet, Numeric, MacAddr, Point, Range, Array
- Binary wire format — per-parameter format codes with binary result decoding
- SCRAM-SHA-256 auth — zero-dep implementation; cleartext password also supported
- Unix domain sockets —
PgConfig.socket_diror?host=URL parameter - Error classification —
ErrorClass::Transient/Permanent/Client/Poolwith SQLSTATE mapping - Retry helper —
retry(max_retries, || { ... })with transient error detection - Production hardening — broken connection flag, TCP_NODELAY, zero-copy writes,
Rc<ColumnDesc>sharing
🚀 Benchmarks
chopin-pg is 1.5–3x faster than async drivers on query throughput due to its synchronous non-blocking architecture and zero external dependencies.
Real-World Performance (localhost PostgreSQL, 100K iterations for simple queries, 10K for CRUD)
Benchmark results from bench_compare — actual run, single connection per driver:
Traditional CRUD
| Workload | chopin-pg | sqlx (tokio) | tokio-postgres | vs sqlx | vs tokio-pg |
|---|---|---|---|---|---|
| SELECT 1 | 50,044 req/s | 17,902 req/s | 22,105 req/s | 2.80x | 2.26x |
| Parameterized Query | 53,405 req/s | 17,840 req/s | 20,283 req/s | 2.99x | 2.63x |
| CRUD SELECT | 47,026 req/s | 17,315 req/s | 18,780 req/s | 2.72x | 2.50x |
| CRUD UPDATE | 15,230 req/s | 9,579 req/s | 9,583 req/s | 1.59x | 1.59x |
| CRUD INSERT | 14,083 req/s | 9,394 req/s | 10,254 req/s | 1.50x | 1.37x |
TFB Multi-Query (500 requests per N)
| N | chopin-pg | sqlx | tokio-postgres | vs sqlx | vs tokio-pg |
|---|---|---|---|---|---|
| 1 | 45,616 req/s | 17,564 req/s | 18,105 req/s | 2.60x | 2.52x |
| 5 | 9,506 req/s | 3,514 req/s | 3,725 req/s | 2.71x | 2.55x |
| 10 | 4,475 req/s | 1,763 req/s | 1,769 req/s | 2.54x | 2.53x |
| 15 | 3,317 req/s | 1,140 req/s | 1,243 req/s | 2.91x | 2.67x |
| 20 | 2,325 req/s | 869 req/s | 918 req/s | 2.68x | 2.53x |
TFB Database Updates (500 requests per N, SELECT+UPDATE each row)
| N | chopin-pg | sqlx | tokio-postgres | vs sqlx | vs tokio-pg |
|---|---|---|---|---|---|
| 1 | 12,068 req/s | 6,443 req/s | 6,370 req/s | 1.87x | 1.89x |
| 5 | 2,137 req/s | 1,272 req/s | 1,228 req/s | 1.68x | 1.74x |
| 10 | 1,047 req/s | 605 req/s | 654 req/s | 1.73x | 1.60x |
| 15 | 624 req/s | 410 req/s | 398 req/s | 1.52x | 1.57x |
| 20 | 525 req/s | 286 req/s | 318 req/s | 1.84x | 1.65x |
Benchmark Configuration:
- 100K iterations for simple queries (SELECT 1, parameterized queries)
- 10K iterations for CRUD operations (SELECT, UPDATE, INSERT)
- 500 requests per TFB Multi-Query count (N=1,5,10,15,20)
- Single connection per driver (no connection pooling overhead)
- Localhost PostgreSQL on port 5432
Why Faster?
- No async runtime overhead — Synchronous
poll()-based I/O eliminates Tokio task scheduler, Future polling, and context switching - Shared-nothing per-worker pools — No lock contention; each worker owns its connections
- Zero external dependencies — Only
libc; no 50+ transitive deps from tokio/sqlx - Hand-tuned protocol — Custom SCRAM-SHA-256, statement cache, CompactBytes inline storage (≤24 bytes)
- CPU affinity — Workers pinned to cores; no task migration
Trade-off: Synchronous API vs. async/await ergonomics. Chopin excels for high-throughput backends (REST APIs, database proxies); less suitable for general async applications.
For detailed benchmark setup, running your own comparisons, and profiling instructions, see:
- BENCHMARKS.md — Detailed methodology & fair comparison guidelines
- RUN_BENCHMARKS.md — Step-by-step execution guide with Docker setup
�🛠️ Quick Start
use ;
🔗 Connection Pool
use ;
use Duration;
let config = from_url?;
// Simple pool
let mut pool = connect?;
// Advanced pool with configuration
let pool_config = new
.max_size
.min_size
.checkout_timeout
.idle_timeout
.max_lifetime
.test_on_checkout;
let mut pool = connect_with_config?;
// Get a connection (auto-returned on drop)
let mut conn = pool.get?;
conn.query_simple?;
// Monitor pool health
println!;
let stats = pool.stats;
println!;
📋 COPY Protocol (Bulk Operations)
// Bulk COPY IN
let mut writer = conn.copy_in?;
writer.write_row?;
writer.write_row?;
let rows_copied = writer.finish?;
println!;
// COPY OUT
let mut reader = conn.copy_out?;
let all_data = reader.read_all?;
println!;
🔔 LISTEN/NOTIFY
conn.listen?;
conn.notify?;
// Poll for notifications
if let Some = conn.poll_notification?
// Drain all buffered notifications
for notif in conn.drain_notifications
conn.unlisten?;
🔄 Transactions
// Closure-based (auto-commit on Ok, auto-rollback on Err)
conn.transaction?;
// Manual control
conn.begin?;
conn.execute?;
conn.commit?;
// Savepoints
conn.begin?;
conn.savepoint?;
conn.execute?;
conn.rollback_to?; // undo Dave
conn.commit?;
// Nested transactions
conn.transaction?;
📊 Supported PostgreSQL Types
| PgValue Variant | PostgreSQL Type | Rust ToSql/FromSql |
|---|---|---|
Bool |
BOOLEAN | bool |
Int2 |
SMALLINT | i16 |
Int4 |
INTEGER | i32 |
Int8 |
BIGINT | i64 |
Float4 |
REAL | f32 |
Float8 |
DOUBLE PRECISION | f64 |
Text |
TEXT, VARCHAR | String, &str |
Bytes |
BYTEA | Vec<u8>, &[u8] |
Json |
JSON | String |
Jsonb |
JSONB | Vec<u8> |
Uuid |
UUID | [u8; 16] |
Date |
DATE | i32 (PG epoch days) |
Time |
TIME | i64 (microseconds) |
Timestamp |
TIMESTAMP | i64 (microseconds) |
Timestamptz |
TIMESTAMPTZ | i64 (microseconds) |
Interval |
INTERVAL | {months, days, microseconds} |
Inet |
INET, CIDR | IpAddr, Ipv4Addr, Ipv6Addr |
Numeric |
NUMERIC | String (lossless precision) |
MacAddr |
MACADDR | [u8; 6] |
Point |
POINT | (f64, f64) |
Range |
INT4RANGE, INT8RANGE, etc. | String |
Array |
ARRAY types | Vec<T> for scalar T |
🔐 Authentication
- SCRAM-SHA-256 — fully implemented with zero external dependencies
- Cleartext password — supported
- MD5 — recognized but returns an error (not implemented)
🔌 Connection Pool Sizing for High Concurrency
When handling high concurrency (e.g., 512+ concurrent connections), proper connection pool sizing is critical. Understanding the relationship between HTTP concurrency and database pool size is essential to avoid connection starvation and timeouts.
Why Pool Size Matters
A common mistake is assuming a 1:1 ratio between concurrent HTTP connections and database pool size. This fails because:
- Not all incoming requests hit the database simultaneously. At any moment, only 30-40% of HTTP connections are actively waiting on DB queries. The rest are parsing requests, serializing responses, or executing in middleware.
- Database connections are expensive. Each connection consumes memory and resources on both the client and server. Creating a connection for every possible concurrent request wastes resources.
- Connection starvation causes cascading failures. If all pool connections are busy and a new request arrives, it must wait. If many requests queue, timeouts increase exponentially.
The Right Formula
Pool Size per Worker = (Total Concurrent Connections / Number of Workers) × Connection Ratio
Connection Ratio (typical): 0.3 to 0.5 (or 2:1 to 5:1 HTTP:DB ratio)
Example: 512 Concurrent Connections
Assuming an 8-core system with 8 workers:
512 connections ÷ 8 workers = 64 connections per worker
❌ Pool size 64 per worker: 64:64 = 1:1 ratio (FAILS - connection starvation)
✅ Pool size 25 per worker: 64:25 = 2.5:1 ratio (RECOMMENDED)
✅ Pool size 20 per worker: 64:20 = 3.2:1 ratio (CONSERVATIVE)
✅ Pool size 32 per worker: 64:32 = 2:1 ratio (IF READ-HEAVY)
Why 64 failed: A 1:1 ratio means every HTTP connection needs its own DB connection. Since DB operations are fast, the pool becomes the bottleneck instead of the database. Requests queue up waiting for available connections, leading to timeouts.
Configuration
Set pool size when initializing the connection pool:
use ;
let config = from_url?;
// For 512 concurrent with 8 workers, use 25 per worker
let pool = new; // ← Recommended starting point
Load Testing Recommendations
After configuring pool size, validate under realistic load:
# Load test with 512 concurrent clients, 8 threads, 30 seconds
# Monitor for:
# - Connection pool timeouts
# - Response latency increases
# - "All connections busy" errors in logs
# Database connection stats (in psql):
) ;
; )
Tuning Guidelines
| Load Pattern | Suggested Pool Size | Ratio | Notes |
|---|---|---|---|
| Read-heavy (80%+ reads) | 30-35 per worker | 2:1 | Queries are fast; can support higher concurrency |
| Balanced (50/50) | 20-25 per worker | 2.5-3.2:1 | Starting point for most workloads |
| Write-heavy (80%+ writes) | 15-20 per worker | 4-5:1 | Queries are slower; queue requests instead |
| Microservices + API calls | 25-40 per worker | 2-3:1 | External latency means more waiting connections |
Monitoring & Alerts
Set up monitoring for pool exhaustion:
// Desired: Log when pool utilization > 80%
// If pool_size=25 and active_connections > 20, investigate
// Symptoms of undersized pool:
// - Increasing avg response time under sustained load
// - Queries queued in pg_stat_activity
// - Application logs: "Pool connection timeout"
// - Database slow query log fills up
Summary
- Never use 1:1 ratio of HTTP connections to DB pool size
- Start with 2.5:1 ratio (20-25 pool size for 512 concurrent / 8 workers)
- Load test under realistic conditions before production deployment
- Monitor pool utilization and adjust based on actual behavior
For 512 concurrent connections, a well-tuned pool of 25 connections per worker will handle typical API workloads efficiently while preventing resource exhaustion.