pg-walstream

July 14, 2026 · View on GitHub

Crates.io Version Crates.io Downloads (recent) Crates.io Total Downloads docs.rs codecov CodSpeed

pg-walstream

A high-performance Rust library for PostgreSQL logical and physical replication protocol parsing and streaming. Provides a robust, type-safe interface for consuming PostgreSQL Write-Ahead Log (WAL) streams.

Features

  • Full Logical Replication Support: Implements PostgreSQL logical replication protocol versions 1-4
  • Physical Replication Support: Stream raw WAL data for standby servers and PITR
  • Base Backup Support: Full BASE_BACKUP command with progress, compression, and manifest options
  • Pure-Rust Backend (default): The default rustls-tls backend needs no libpq and no OpenSSL, using aws-lc-rs for hardware-accelerated TLS (AES-NI, AVX2, SHA-NI).
  • TLS/SSL Support: All PostgreSQL SSL modes (disable, allow, prefer, require, verify-ca, verify-full)
  • Authentication: Cleartext, MD5, and SCRAM-SHA-256 authentication methods
  • Streaming Transactions: Support for streaming large transactions (protocol v2+)
  • Two-Phase Commit: Prepared transaction support (protocol v3+)
  • Parallel Streaming: Multi-stream parallel replication (protocol v4+)
  • Zero-Copy Operations: Efficient buffer management using the bytes crate with drain-loop batch queue optimization. The libpq backend copies each COPY message into a reusable BytesMut buffer, then reference-counts it downstream as Bytes (no per-message heap allocation after warmup)
  • Thread-Safe LSN Tracking: Atomic LSN feedback for producer-consumer patterns
  • Connection Management: Built-in connection handling with exponential backoff retry logic
  • Type-Safe API: Strongly typed message parsing with comprehensive error handling
  • Typed Row Deserialization: Built-in serde deserializer maps WAL rows directly into user-defined Rust structs (numerics, bool, String, Option<T>, enums, bytes)
  • High-Level Consumption Ergonomics: ReplicationStreamConfig::builder(), an auto-acking EventStream::for_each_event, and a typed by-table WalRouter with an optional #[derive(WalTable)] layer (opt-in derive feature)
  • Bounded Replay: ReplicationStreamConfig::with_stop_at_lsn streams to a target LSN, delivers the crossing transaction in full, then ends cleanly with ReplicationError::StreamStopped
  • Raw XLogData Access: LogicalReplicationStream::next_raw_event yields the undecoded pgoutput payload plus WAL positions (RawXLogData) for consumers that bring their own decoder — keepalives, feedback, and cancellation still handled, no auto-ack
  • Replication Slot Management: Create, alter, read, and drop slots with full option support
  • Hot Standby Feedback: Send hot standby feedback messages for physical replication

Installation

Add this to your Cargo.toml:

[dependencies]
pg_walstream = "0.8"

By default, this uses the pure-Rust rustls-tls backend — no libpq and no OpenSSL, only cmake + a C compiler at build time (for aws-lc-rs).

To use the C libpq backend instead (bound by pq-sys; requires system libpq):

[dependencies]
pg_walstream = { version = "0.8", default-features = false, features = ["libpq"] }

If both backends are enabled, rustls-tls takes priority automatically.

Feature Flags

pg-walstream provides two connection backends plus a std toggle, all selected at compile time. rustls-tls is the default; libpq is opt-in. When both are enabled, rustls-tls takes priority:

FeatureDefaultC DependenciesDescription
stdYesNoneStandard library support. Disable with default-features = false for a no_std plus alloc parser-only build (no connection layer) that compiles for wasm32-unknown-unknown and embedded targets.
libpqNolibpq-dev + OpenSSLOpt-in. PostgreSQL's C client library via FFI, bound by pq-sys (pre-generated bindings — no libclang). Battle-tested. Enable with --no-default-features --features libpq. Implies std.
rustls-tlsYescmake, gcc (build-time only)Default. Pure-Rust implementation using rustls with aws-lc-rs crypto backend for hardware-accelerated TLS. No libpq, no OpenSSL, no runtime C dependencies. Takes priority when both backends are enabled. Implies std.
deriveNoNoneOpt-in proc-macros (pull syn/quote) that bind a struct to a table — #[derive(WalTable)] #[wal(table = "...")] or the one-line attribute form #[wal_table("...")] — enabling the WalRouter::on_insert_of::<T> / on_update_of::<T> / on_delete_of::<T> table-inference methods.

Note: The protocol parser, encoder, and types need no backend. Building with default-features = false gives a no_std plus alloc build of just those, suitable for wasm and embedded. A connection backend (libpq or rustls-tls) is required only for the live streaming and connection APIs, and pulls in std.

System Dependencies

System dependencies are only required for the opt-in libpq feature. The default rustls-tls backend requires only cmake and a C compiler at build time (for the aws-lc-rs crypto library), with no runtime dependencies.

For libpq feature (opt-in)

Ubuntu/Debian:

sudo apt-get install libpq-dev libssl-dev

CentOS/RHEL/Fedora:

sudo yum install postgresql-devel
# or
sudo dnf install postgresql-devel

For rustls-tls feature

Requires cmake and a C compiler at build time for aws-lc-rs (hardware-accelerated cryptography):

Ubuntu/Debian:

sudo apt-get install cmake gcc

Then add to Cargo.toml:

pg_walstream = { version = "0.8", features = ["rustls-tls"] }

TLS trust store

When sslmode is verify-ca or verify-full, the rustls-tls backend builds its root certificate store as follows:

  1. If sslrootcert is set, it loads only those CAs from the PEM file (exclusive).
  2. Otherwise, it loads the Mozilla CA bundle shipped via webpki-roots.

The OS trust store is not consulted. If your PostgreSQL server is signed by a corporate/internal CA that is only present in the OS trust store (e.g. /etc/ssl/certs), you must point sslrootcert at that CA explicitly — for example:

postgresql://user:pass@host/db?sslmode=verify-full&sslrootcert=/etc/ssl/certs/corporate-ca.pem

Quick Start

The examples/ directory contains runnable examples demonstrating various usage patterns:

ExampleDescription
basic-streamingHigh-level futures::Stream API with stream combinators (filter, take_while)
pollingManual polling loop using next_event() for custom integration scenarios
safe-transaction-consumerProduction-grade transaction-aware CDC consumer with ordered commits and safe LSN feedback
rate-limited-streamingRate-limited consumption using tokio_stream::StreamExt::throttle
tokio-spawn-streamingProducer/consumer pattern via tokio::spawn with mpsc channel (demonstrates Send safety)
typed-deserializationMap INSERT/UPDATE/DELETE events directly into user-defined Rust structs via serde
derive-router#[derive(WalTable)] + WalRouter table-inference (on_*_of::<T>) — the derive feature
pg-basebackupFull physical backup tool using BASE_BACKUP with tar extraction and progress reporting
arbitrary-fuzzingProperty-based fuzzing of all protocol types using the arbitrary crate

For more control, you can use the traditional polling approach:

use pg_walstream::{
    LogicalReplicationStream, ReplicationStreamConfig, RetryConfig, StreamingMode,
    SharedLsnFeedback, CancellationToken,
};
use std::sync::Arc;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ReplicationStreamConfig::new(
        "my_slot".to_string(),
        "my_publication".to_string(),
        2, StreamingMode::On,
        Duration::from_secs(10),
        Duration::from_secs(30),
        Duration::from_secs(60),
        RetryConfig::default(),
    );

    let mut stream = LogicalReplicationStream::new(
        "postgresql://postgres:password@localhost:5432/mydb?replication=database",
        config,
    ).await?;

    stream.start(None).await?;

    let cancel_token = CancellationToken::new();

    // Traditional polling loop with automatic retry
    loop {
        match stream.next_event_with_retry(&cancel_token).await {
            Ok(event) => {
                println!("Received event: {:?}", event);
                stream.shared_lsn_feedback.update_applied_lsn(event.lsn.value());
            }
            Err(e) if matches!(e, pg_walstream::ReplicationError::Cancelled(_)) => {
                println!("Cancelled, shutting down gracefully");
                break;
            }
            Err(e) => {
                eprintln!("Error: {}", e);
                break;
            }
        }
    }

    Ok(())
}

Mapping Columns to Struct Fields

Rows are keyed by their real PostgreSQL column names. When a struct field name differs from its column, use serde's #[serde(rename = "...")] — no extra attribute is needed:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct User {
    id: i64,
    #[serde(rename = "user_name")] // field `username` ← column `user_name`
    username: String,
    #[serde(rename = "mail")]      // field `email`    ← column `mail`
    email: Option<String>,         // nullable column → Option
}

// let user: User = event.deserialize_insert()?;   // or row.deserialize_into()?

With the derive feature, add #[wal_table("...")] (outermost attribute) to bind the type to its table for WalRouter; it composes with the renames above:

#[wal_table("typed_deser_users")]
#[derive(Debug, Deserialize)]
struct User { /* fields as above */ }

LSN Tracking

Thread-safe LSN tracking for feedback to PostgreSQL:

use pg_walstream::SharedLsnFeedback;
use std::sync::Arc;

let feedback = SharedLsnFeedback::new_shared();

// Producer thread: read LSN from feedback
let (flushed_lsn, applied_lsn) = feedback.get_feedback_lsn();

// Consumer thread: update LSN after processing
feedback.update_applied_lsn(commit_lsn);

PostgreSQL Setup

Before using this library, you need to configure PostgreSQL for replication:

1. Configure PostgreSQL

Edit postgresql.conf:

wal_level = logical
max_replication_slots = 4
max_wal_senders = 4

Restart PostgreSQL after making these changes.

2. Create a Publication

-- Create a publication for specific tables
CREATE PUBLICATION my_publication FOR TABLE users, orders;

-- Or publish all tables
CREATE PUBLICATION my_publication FOR ALL TABLES;

3. Create Replication User

-- Create a user with replication privileges
CREATE USER replication_user WITH REPLICATION PASSWORD 'secure_password';

-- Grant necessary permissions
GRANT SELECT ON ALL TABLES IN SCHEMA public TO replication_user;
GRANT USAGE ON SCHEMA public TO replication_user;

4. Replication Slot Options

The library provides full control over replication slot creation. The correct SQL syntax is automatically selected based on the connected PostgreSQL version:

  • PG14: Legacy positional keyword syntax (EXPORT_SNAPSHOT, NOEXPORT_SNAPSHOT, USE_SNAPSHOT, TWO_PHASE, RESERVE_WAL)
  • PG15+: Modern parenthesized options syntax ((SNAPSHOT 'export', TWO_PHASE true, ...))
OptionDescriptionPG Version
temporaryTemporary slot (not persisted to disk, dropped on disconnect)14+
two_phaseEnable two-phase commit for logical slots14+
reserve_walReserve WAL immediately for physical slots14+
snapshotSnapshot behavior: "export", "use", or "nothing"14+
failoverEnable slot synchronization to standbys for HA16+

Note: If both two_phase and snapshot are set, two_phase takes priority. The failover option is not available on PG14 and will return an error.

Message Types

The library supports all PostgreSQL logical replication message types:

Protocol Version 1 Messages

  • BEGIN: Transaction start
  • COMMIT: Transaction commit
  • ORIGIN: Replication origin
  • RELATION: Table schema definition
  • TYPE: Data type definition
  • INSERT: Row insertion
  • UPDATE: Row update
  • DELETE: Row deletion
  • TRUNCATE: Table truncation
  • MESSAGE: Generic message

Protocol Version 2+ Messages (Streaming)

  • STREAM_START: Streaming transaction start
  • STREAM_STOP: Streaming transaction segment end
  • STREAM_COMMIT: Streaming transaction commit
  • STREAM_ABORT: Streaming transaction abort

Protocol Version 3+ Messages (Two-Phase Commit)

  • BEGIN_PREPARE: Prepared transaction start
  • PREPARE: Transaction prepare
  • COMMIT_PREPARED: Commit prepared transaction
  • ROLLBACK_PREPARED: Rollback prepared transaction
  • STREAM_PREPARE: Stream prepare message

Architecture

┌──────────────────────────────────────────┐
│          Application Layer               │
│  (Your CDC / Replication Logic)          │
└──────────────┬───────────────────────────┘

┌──────────────▼───────────────────────────┐
│    LogicalReplicationStream              │
│  - Connection management & retry         │
│  - Event processing & LSN feedback       │
│  - Snapshot export support               │
└──────────────┬───────────────────────────┘

┌──────────────▼───────────────────────────┐
│  LogicalReplicationParser                │
│  - Protocol v1-v4 parsing                │
│  - Zero-copy message deserialization     │
│  - Streaming transaction support         │
└──────────────┬───────────────────────────┘

┌──────────────▼───────────────────────────┐
│     PgReplicationConnection              │
│  ┌─────────────────┬──────────────────┐  │
│  │  libpq backend  │ rustls-tls       │  │
│  │  (C FFI)        │ (pure Rust)      │  │
│  │                 │                  │  │
│  │  pq-sys         │ rustls +         │  │
│  │                 │ aws-lc-rs +      │  │
│  │                 │ postgres-protocol│  │
│  └─────────────────┴──────────────────┘  │
│  Compile-time feature flag selection     │
└──────────────┬───────────────────────────┘

┌──────────────▼───────────────────────────┐
│     BufferReader / BufferWriter          │
│  - Zero-copy operations (bytes crate)    │
│  - Binary protocol handling              │
│  - Drain-loop batch queue optimization   │
└──────────────────────────────────────────┘

Stress Test & System Threshold Analysis

Progressive writer concurrency ramp (16 - 192 writers) to find the library's CPU saturation point and throughput ceiling.

  • Backend A: rustls-tls
  • Backend B: libpq

Test environment: an 8-vCPU Linux VM (TCP-tuned per Linux VM TCP Tuning: 64 MB buffers, BBR) streaming from a remote Azure PostgreSQL Flexible Server 18.4 over TLS (sslmode=require). Each scenario ran 10 s warmup + 30 s measure; every configuration was measured 3 times per backend and the median is reported (cross-run CoV ≤ ~6%). Process CPU/RSS reflect only the pg-walstream consumer — the write generator runs as a separate OS process.

Reading these numbers: Over a real network link the consumer spends most of its time parked in epoll/TLS I/O rather than parsing, so both backends are network-I/O-bound and converge to within run-to-run noise on CPU efficiency, CPU%, RSS, and latency. rustls-tls keeps a small (~8%) throughput edge on saturated single-stream ingest. If you are benchmarking over loopback/localhost you may see larger CPU gaps; those do not represent a realistic remote-DB deployment.

1. CPU Efficiency (DML events/sec per 1% CPU)

This is the primary efficiency metric: how many DML events each backend processes for every 1% of CPU it consumes. Higher is better.

Scenariorustls-tlslibpqDeltaWinner
Baseline4,3384,250+2.1%~tie
Batch-1002,4592,575-4.5%~tie
Batch-50004,1054,122-0.4%~tie
4-Writers3,6213,626-0.1%~tie
Wide-20col1,8561,960-5.3%~tie
Payload-2KB1,2121,218-0.5%~tie
Mixed-DML3,2933,301-0.2%~tie

Both backends land within ±5% across every scenario — a statistical tie.

2. Throughput Comparison

Scenariorustls-tls ev/slibpq ev/sDeltarustls-tls DML/slibpq DML/sDeltaWinner
Baseline178,812164,585+8.6%177,414163,299+8.6%rustls-tls
Batch-10030,90931,069-0.5%30,30330,460-0.5%~tie
Batch-5000167,095153,424+8.9%165,801152,236+8.9%rustls-tls
4-Writers147,377146,476+0.6%145,273144,404+0.6%~tie
Wide-20col27,12627,434-1.1%26,13326,429-1.1%~tie
Payload-2KB23,06921,297+8.3%21,40019,756+8.3%rustls-tls
Mixed-DML55,18057,506-4.0%54,52656,824-4.0%libpq

rustls-tls is ~8–9% faster on saturated single-stream ingest (Baseline, Batch-5000, Payload-2KB); everything else is within noise.

3. Resource Utilization Comparison

Process CPU and RSS reflect only the pg-walstream consumer (generator runs as a separate OS process).

Scenariorustls-tls CPU%libpq CPU%Deltarustls-tls RSS MBlibpq RSS MBDeltaWinner
Baseline38.438.4-0.0%14.814.8-0.1%~tie
Batch-10012.312.1+1.7%16.115.9+1.3%~tie
Batch-500038.937.2+4.6%16.316.3-0.2%~tie
4-Writers39.639.7-0.3%16.516.5+0.4%~tie
Wide-20col14.114.4-2.4%16.616.6+0.2%~tie
Payload-2KB17.916.2+10.5%16.516.7-0.9%libpq
Mixed-DML16.517.7-6.4%16.717.6-5.1%rustls-tls

CPU% and RSS are effectively equal; the largest single-scenario deltas (~10%) sit inside the 3-run variance.

4. Latency Comparison (inter-event, microseconds)

Scenariorustls-tls P50libpq P50rustls-tls P99libpq P99Winner
Baseline117266~tie
Batch-10011645599~tie
Batch-5000116464~tie
4-Writers11150149~tie
Wide-20col11227223~tie
Payload-2KB55390365~tie
Mixed-DML11107109~tie

P50 is 1 µs for both; P99 tracks within ±10 µs — no backend advantage.

5. Stress Ramp Comparison

Progressive writer concurrency ramp — comparing throughput and CPU scaling.

Writersrustls-tls DML/slibpq DML/sDeltarustls-tls CPU%libpq CPU%rustls-tls efflibpq eff
16127,226125,729+1.2%42.943.42,9752,969
32115,728117,595-1.6%42.442.12,7642,770
48103,432105,136-1.6%40.640.02,5912,671
6498,47399,038-0.6%37.837.32,6162,586
9687,36187,592-0.3%34.035.52,5502,468
12879,70976,466+4.2%32.934.02,4252,306
19272,29172,186+0.1%33.132.92,1932,194

Throughput and efficiency scale essentially identically for both backends across the ramp.

Peak Numbers

Metricrustls-tlslibpq
Peak DML events/sec177,414163,299
Peak total events/sec178,812164,585
Peak CPU efficiency (DML/s per 1% CPU)4,3384,250
Peak process CPU%5150
Peak RSS (MB)1818

For a detailed comparison across PostgreSQL 16 and 18 with different optimizations (binary mode, direct TLS, COPY protocol), see the Load Test Comparison Report.

Linux VM TCP Tuning for Production

When streaming WAL over high-latency links (e.g., cross-region Azure PostgreSQL), the default Linux TCP buffer sizes can become the throughput bottleneck. The kernel's default rmem_max of 208 KB limits the TCP receive window, which — combined with round-trip latency — caps throughput via the Bandwidth-Delay Product (BDP):

# --- TCP buffer sizes ---
# Allow up to 64 MB per-socket receive/send buffers (kernel will auto-tune within this ceiling)
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864

# TCP auto-tuning ranges: min / default / max (bytes)
# The kernel dynamically adjusts each socket's buffer within these bounds
net.ipv4.tcp_rmem = 4096 262144 67108864
net.ipv4.tcp_wmem = 4096 262144 67108864

# --- Congestion control ---
# BBR provides significantly better throughput than cubic on high-latency links
net.ipv4.tcp_congestion_control = bbr

# --- Packet backlog ---
# Increase the NIC receive queue (helps at high packet rates)
net.core.netdev_max_backlog = 5000

Apply immediately:

sudo sysctl --system

Why Each Parameter Matters

ParameterDefaultRecommendedWhy
rmem_max208 KB64 MBCaps TCP receive window; directly limits throughput on high-RTT links
wmem_max208 KB64 MBCaps TCP send window; limits outbound throughput for feedback messages
tcp_rmem (max)6 MB64 MBPer-socket auto-tuned receive buffer ceiling
tcp_wmem (max)4 MB64 MBPer-socket auto-tuned send buffer ceiling
tcp_congestion_controlcubicbbrBBR reacts to actual bandwidth, not packet loss; better on cloud networks
netdev_max_backlog10005000Prevents packet drops under burst traffic at NIC level

Note: These settings affect all TCP connections on the VM, not just pg-walstream. The kernel auto-tunes actual buffer usage within the configured ceiling, so idle connections do not consume 64 MB each.

Limitations

  • Requires PostgreSQL 14 or later for full protocol support
  • Logical replication slot must be created before streaming
  • Binary protocol only (no text-based protocol support)
  • Requires replication permission for the database user

Resources

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the BSD 3-Clause License.

Author

Daniel Shih (dog830228@gmail.com)