PromptHub
Back to Blog
Developer Tools Data Engineering

Duckgres Exposed: Why Developers Are Ditching Traditional Postgres for Analytical Workloads

B

Bright Coding

Author

14 min read 55 views
Duckgres Exposed: Why Developers Are Ditching Traditional Postgres for Analytical Workloads

Duckgres Exposed: Why Developers Are Ditching Traditional Postgres for Analytical Workloads

What if I told you that your PostgreSQL↗ Bright Coding Blog queries could run 10-100x faster on analytical workloads—without changing a single line of application code? No migration scripts. No ORM rewrites. No client library swaps.

Here's the brutal truth developers face daily: PostgreSQL is the undisputed king of transactional workloads, but it becomes a performance nightmare when you throw complex analytics at it. Massive GROUP BY operations. Window functions over billions of rows. Ad-hoc exploratory queries that scan entire tables. Your carefully tuned B-tree indexes? Useless. Your query planner? Drowning in sequential scans.

You've probably tried the workarounds. Read replicas for analytical offloading. Columnar extensions that feel bolted-on. ETL pipelines dumping data into separate warehouses. Each "solution" introduces operational complexity, data freshness delays, and maintenance overhead that kills productivity.

But what if you could keep your existing PostgreSQL clients—psql, pgAdmin, psycopg2, lib/pq, JDBC—and magically swap the engine underneath? That's exactly what Duckgres delivers. Built by the engineering team at PostHog, this open-source project is a PostgreSQL wire-protocol compatible server backed by DuckDB, the blazing-fast analytical database taking the data world by storm.

Ready to see how it works? Let's dive deep.


What Is Duckgres?

Duckgres is a PostgreSQL protocol server that translates your existing PostgreSQL connections into DuckDB's analytical engine. Created by PostHog—the popular open-source product analytics platform—Duckgres emerged from real production needs: running complex analytical queries at scale without fragmenting their data infrastructure.

The project's mascot, a charming duck-Postgres hybrid, perfectly captures its identity: PostgreSQL on the outside, DuckDB on the inside.

Why It's Trending Now

DuckDB has exploded in popularity because it brings columnar analytical performance to embedded and local workflows. But there's a catch: most tools, dashboards, and applications speak PostgreSQL wire protocol, not DuckDB's native interface. Duckgres bridges this gap elegantly.

PostHog open-sourced Duckgres because they needed it for their own managed data warehouse product. Rather than maintaining a private fork, they bet on community adoption—and developers are responding. The repository combines production-hardened features like TLS encryption, multi-process isolation, Kubernetes-native deployment, and Prometheus metrics with the simplicity of a single binary.

In an era where "data lakehouse" architectures dominate conversations, Duckgres offers a pragmatic shortcut: lakehouse-grade performance with PostgreSQL-grade compatibility.


Key Features That Make Duckgres Insane

Duckgres isn't a toy project. It's battle-tested infrastructure with features that enterprise teams pay six figures for in proprietary warehouses.

Full PostgreSQL Wire Protocol Compatibility

Connect with any PostgreSQL client. psql, pgAdmin, DBeaver, Tableau, Metabase, Python↗ Bright Coding Blog's psycopg2, Go's lib/pq, Rust's tokio-postgres, Node's pg—they all work out of the box. Duckgres handles TLS termination, password authentication, extended query protocol with prepared statements, and binary result formats.

Two-Tier Query Processing

This is Duckgres's secret sauce. Every incoming query first attempts parsing with PostgreSQL's native parser (libpg_query). If valid, it gets transpiled to DuckDB SQL with proper type mappings. If PostgreSQL parsing fails, Duckgres falls back to direct DuckDB validation via EXPLAIN. This transparently enables DuckDB-specific superpowers: FROM-first queries, SELECT * EXCLUDE/REPLACE, QUALIFY clauses, ASOF joins, lambda functions, and SAMPLE—all through your standard PostgreSQL client.

Production-Grade Security

TLS is required, not optional. Self-signed certificates auto-generate on first run. Password authentication uses cleartext over TLS (standard PostgreSQL pattern). Built-in rate limiting protects against brute-force attacks with configurable IP bans and connection limits.

Per-User Database Isolation

Each authenticated user receives their own isolated DuckDB database file. No cross-user data leakage. No permission nightmares. Files live in configurable data directories with automatic creation.

Control Plane Architecture

For production deployments, Duckgres offers multi-process mode: a control plane manages client connections while worker processes handle DuckDB execution. This enables zero-downtime deployments via socket handover, rolling worker updates via SIGUSR2, and resource isolation between tenants. Kubernetes-native remote workers extend this to multi-tenant cloud deployments.

DuckLake & Delta Lake Integration

Duckgres auto-attaches DuckLake catalogs for SQL-based lakehouse workflows. Configure S3-compatible object storage (MinIO, AWS↗ Bright Coding Blog S3) and optionally attach Delta Lake tables alongside. Your PostgreSQL client suddenly queries petabyte-scale lakehouses.

Observable by Default

Prometheus metrics expose connection counts, query durations, error rates, authentication failures, rate-limiting events, and control-plane worker states. Grafana dashboards? One docker↗ Bright Coding Blog compose away.


4 Real-World Use Cases Where Duckgres Dominates

1. Analytics Dashboard Acceleration

Your product team demands real-time analytics, but your PostgreSQL read replicas choke on COUNT(DISTINCT ...) across millions of events. Instead of provisioning a separate ClickHouse or BigQuery instance, point Metabase or Apache Superset at Duckgres. Same dashboards, dramatically faster queries, zero migration effort.

2. Local Development & Testing

Data engineers need production-like datasets locally without managing PostgreSQL containers. Duckgres starts in seconds, creates per-user databases automatically, and handles analytical queries that would crush a local Postgres instance. Run just run and connect with just psql—development environment solved.

3. ETL Pipeline Staging

Before loading transformed data to your cloud warehouse, validate it with Duckgres. Use the COPY protocol for bulk import/export, run complex analytical validations with DuckDB's performance, then export to final destination. The PostgreSQL compatibility means existing Airflow operators, dbt adapters, and Python scripts work unchanged.

4. Multi-Tenant Managed Warehouses

PostHog's own use case: offer isolated analytical databases per customer without maintaining separate PostgreSQL clusters. Control-plane mode with Kubernetes remote workers spins up isolated DuckDB instances per tenant, attaches their DuckLake catalogs, and exposes standard PostgreSQL endpoints. Scale workers independently, upgrade versions without downtime, and monitor everything through Prometheus.


Step-by-Step Installation & Setup Guide

Prerequisites

  • Go 1.21+ (for building from source)
  • just command runner
  • Docker (optional, for containerized deployment)

Build from Source

# Clone the repository
git clone https://github.com/PostHog/duckgres.git
cd duckgres

# See all available commands
just

# Build the binary
just build

# Run in standalone mode (default port 5432, TLS enabled)
just run

The server auto-generates self-signed certificates in ./certs/ and stores database files in ./data/.

Connect with psql

# Connect via psql using the just recipe (handles SSL mode)
just psql

# Or connect on a custom port
just psql 35437

# Manual connection with password authentication
PGPASSWORD=postgres psql "host=localhost port=5432 user=postgres sslmode=require"

Docker Deployment

# Build the image (tagged duckgres:dev)
just docker

# Run with exposed ports
docker run --rm -p 5432:5432 -p 9090:9090 duckgres:dev

# Production: mount config and persist data
docker run --rm \
  -p 5432:5432 -p 9090:9090 \
  -v ./duckgres.yaml:/app/duckgres.yaml \
  -v ./data:/app/data \
  duckgres:dev

Configuration File Setup

Create duckgres.yaml for production settings:

host: "0.0.0.0"
port: 5432
data_dir: "./data"
session_init_timeout: "10s"

tls:
  cert: "./certs/server.crt"
  key: "./certs/server.key"

users:
  postgres: "postgres"
  analytics_user: "secure_password_here"

extensions:
  - ducklake
  - httpfs

ducklake:
  metadata_store: "postgres:host=localhost user=ducklake password=secret dbname=ducklake"
  disable_metadata_thread_local_cache: true

rate_limit:
  max_failed_attempts: 5
  failed_attempt_window: "5m"
  ban_duration: "15m"
  max_connections_per_ip: 100

Run with config:

./duckgres --config duckgres.yaml

Environment Variables (12-Factor Style)

export DUCKGRES_HOST=0.0.0.0
export DUCKGRES_PORT=5432
export DUCKGRES_DATA_DIR=./data
export DUCKGRES_MEMORY_LIMIT=8GB
export DUCKGRES_THREADS=8
export POSTHOG_API_KEY=phc_your_project_api_key  # Optional: structured logging

./duckgres

Control Plane Mode (Production)

# Start control plane with pre-warmed workers
./duckgres --mode control-plane \
  --port 5432 \
  --process-min-workers 2 \
  --process-max-workers 10 \
  --flight-port 8815  # Enable Arrow Flight SQL ingress

# Zero-downtime deployment with handover socket
./duckgres --mode control-plane \
  --port 5432 \
  --handover-socket /var/run/duckgres/handover.sock

REAL Code Examples from Duckgres

Let's examine actual code patterns from the Duckgres repository, with detailed explanations of how they leverage DuckDB's analytical engine through PostgreSQL compatibility.

Example 1: Basic Table Creation and Analytical Query

This example from the README demonstrates core CRUD operations with DuckDB's aggregation performance:

-- Create a standard table using PostgreSQL-compatible syntax
CREATE TABLE events (
    id INTEGER,
    name VARCHAR,
    timestamp TIMESTAMP,
    value DOUBLE
);

-- Insert multiple rows in a single statement
INSERT INTO events VALUES
    (1, 'click', '2024-01-01 10:00:00', 1.5),
    (2, 'view', '2024-01-01 10:01:00', 2.0);

-- Run analytical aggregations with DuckDB's columnar engine
-- This executes with vectorized processing, not PostgreSQL's row-by-row approach
SELECT name, COUNT(*), AVG(value)
FROM events
GROUP BY name;

What's happening under the hood? The CREATE TABLE and INSERT statements parse through Tier 1 (PostgreSQL parser), get transpiled to DuckDB SQL, and execute against DuckDB's in-process engine. The SELECT with GROUP BY leverages DuckDB's vectorized execution—processing data in compressed columnar batches rather than iterating rows. Your psql client receives standard PostgreSQL binary results, completely unaware of the translation.

Example 2: DuckDB-Specific Syntax via Two-Tier Fallback

Duckgres's two-tier processing enables DuckDB superpowers that don't exist in PostgreSQL:

-- FROM-first query: DuckDB's ergonomic syntax, not valid PostgreSQL SQL
-- Tier 1 (PostgreSQL parser) FAILS → falls through to Tier 2 (DuckDB validation)
FROM events
SELECT name, COUNT(*)
WHERE value > 1.0
GROUP BY name;

-- EXCLUDE columns dynamically: another DuckDB-exclusive feature
SELECT * EXCLUDE (timestamp) FROM events;

-- SAMPLE for approximate queries on large datasets
SELECT name, AVG(value) FROM events USING SAMPLE 10%;

-- ASOF joins for temporal analysis (event sequences, financial data)
SELECT *
FROM events ASOF JOIN other_events
  ON events.timestamp >= other_events.timestamp;

The magic here: These queries would fail in actual PostgreSQL. FROM-first syntax, EXCLUDE, SAMPLE, and ASOF joins are DuckDB innovations. Duckgres's Tier 2 fallback catches the PostgreSQL parse failure, validates against DuckDB via EXPLAIN, and executes natively. Your BI tools, ORMs, and scripts receive results as if PostgreSQL supported these features all along.

Example 3: Bulk Data Operations with COPY Protocol

Production ETL pipelines depend on efficient bulk transfer. Duckgres implements PostgreSQL's native COPY protocol:

-- Export entire table as tab-separated (PostgreSQL default)
COPY events TO STDOUT;

-- Export as CSV with headers for Excel/BI tool consumption
COPY events TO STDOUT WITH CSV HEADER;

-- Export filtered query results directly
COPY (
    SELECT * FROM events 
    WHERE timestamp > '2024-01-01' AND value > 1.0
) TO STDOUT WITH CSV;

-- Import bulk data from stdin (use with psql \copy or programmatic drivers)
COPY events FROM STDIN;
-- Then paste or stream tab-separated data, terminate with \.

-- Import CSV with automatic header parsing
COPY events FROM STDIN WITH CSV HEADER;

Performance insight: COPY bypasses the extended query protocol's per-row overhead. For DuckDB, this means direct columnar bulk loading into its native storage format. A million rows that might take minutes through individual INSERTs load in seconds. The \copy command in psql wraps this protocol, making it accessible interactively.

Example 4: DuckLake Lakehouse Configuration

Here's the real configuration for connecting to S3-compatible object storage with Delta Lake support:

ducklake:
  # PostgreSQL metadata catalog connection
  metadata_store: "postgres:host=localhost port=5433 user=ducklake password=ducklake dbname=ducklake"
  
  # S3 object storage path for DuckLake data files
  object_store: "s3://ducklake/data/"
  
  # Also attach Delta Lake tables at s3://ducklake/delta/ automatically
  delta_catalog_enabled: true
  
  # Explicit credentials for MinIO or S3-compatible storage
  s3_provider: "config"
  s3_endpoint: "localhost:9000"
  s3_access_key: "minioadmin"
  s3_secret_key: "minioadmin"
  s3_region: "us-east-1"
  s3_use_ssl: false
  s3_url_style: "path"  # "path" for MinIO, "vhost" for AWS S3

Architecture insight: This configuration creates a lakehouse architecture where DuckDB queries Parquet files in S3 through DuckLake's metadata catalog. The delta_catalog_enabled option simultaneously attaches Delta Lake tables—enabling time travel queries, ACID transactions on lakehouse data, and schema evolution that raw Parquet lacks. Your PostgreSQL client queries this as if it were local tables.

Example 5: Seeding Sample Data for Exploration

The repository includes a seed script for immediate experimentation:

# Populate with e-commerce and analytics sample data
./scripts/seed_ducklake.sh --clean  # Clean existing, then reseed

# Or with custom connection parameters
./scripts/seed_ducklake.sh \
  --host 127.0.0.1 \
  --port 5432 \
  --user postgres \
  --password postgres

After seeding, run analytical queries:

-- Top products by price: typical BI question
SELECT name, price FROM products ORDER BY price DESC LIMIT 5;

-- Join orders with customer data: relational analytics
SELECT o.id, c.first_name, c.last_name, o.total_amount, o.status
FROM orders o 
JOIN customers c ON o.customer_id = c.id;

-- Event funnel analysis: product analytics pattern
SELECT event_name, COUNT(*) 
FROM events 
GROUP BY event_name 
ORDER BY COUNT(*) DESC;

Advanced Usage & Best Practices

Memory and Thread Tuning

DuckDB's performance depends heavily on resource allocation. Set per-session limits:

export DUCKGRES_MEMORY_LIMIT=16GB
export DUCKGRES_THREADS=16  # Match your CPU cores for analytical workloads

For control-plane mode, use --memory-budget to cap total across all workers.

Process Isolation for Security

Enable --process-isolation to spawn a separate OS process per connection. Slightly higher latency, but complete memory isolation between users—critical for multi-tenant deployments.

Graceful Shutdown in Production

Configure shutdown behavior to prevent query interruption:

// Go configuration example from the codebase
cfg := server.Config{
    ShutdownTimeout: 60 * time.Second,  // Wait for in-flight queries
}

In systemd, set RuntimeDirectoryPreserve=yes when using handover sockets for zero-downtime deployments.

Worker Pre-warming

Cold starts hurt user experience. In control-plane mode:

./duckgres --mode control-plane --process-min-workers 5 --process-max-workers 50

This maintains 5 warm workers, scaling to 50 under load. Workers retire after session idle time unless --process-retire-on-session-end is set.

Monitoring with Prometheus

Key alerts to configure:

  • duckgres_query_errors_total spike → investigate failing queries
  • duckgres_rate_limited_ips growth → potential attack or misconfiguration
  • duckgres_control_plane_worker_queue_depth > 10 → scale workers or investigate bottlenecks

Comparison with Alternatives

Feature Duckgres PostgreSQL + Citus ClickHouse Apache Druid BigQuery
PostgreSQL Protocol ✅ Native ✅ Native ❌ Custom HTTP ❌ Custom HTTP ❌ Custom
Zero Client Changes ✅ Yes ⚠️ Schema changes ❌ No ❌ No ❌ No
Columnar Storage ✅ DuckDB ❌ Row-based ✅ Native ✅ Native ✅ Native
Embedded/Local ✅ Single binary ❌ Distributed ❌ Server ❌ Cluster ❌ Cloud-only
Lakehouse (S3/Delta) ✅ DuckLake ❌ No ⚠️ Limited ⚠️ Limited ✅ BigLake
Open Source ✅ MIT ✅ AGPL ✅ Apache 2.0 ✅ Apache 2.0 ❌ Proprietary
Operational Complexity Low High Medium High None (managed)
Cost Free Free + infra Free + infra Free + infra Pay-per-query

When to choose Duckgres:

  • You need PostgreSQL compatibility without performance sacrifices on analytics
  • You want local development with production-grade analytical performance
  • You're building multi-tenant data products with per-user isolation
  • You need lakehouse architecture without vendor lock-in

When to look elsewhere:

  • Pure transactional OLTP workloads (vanilla PostgreSQL wins)
  • Sub-second streaming analytics (ClickHouse or Flink)
  • Petabyte-scale without any local processing (BigQuery/Snowflake)

FAQ

Is Duckgres a fork of PostgreSQL?

No. Duckgres is a standalone Go application that implements the PostgreSQL wire protocol. It doesn't use PostgreSQL source code—it translates PG protocol messages to DuckDB SQL and returns results in PostgreSQL format.

Can I use my existing ORM with Duckgres?

Absolutely. Django ORM, SQLAlchemy, ActiveRecord, Prisma, GORM—if it connects to PostgreSQL, it works with Duckgres. The pg_catalog and information_schema implementations satisfy introspection queries.

How does transaction isolation differ from PostgreSQL?

Duckgres uses DuckDB's snapshot isolation (MVCC), which is stricter than PostgreSQL's default read committed. Non-repeatable reads and phantom reads are impossible. The only visible difference: concurrent write conflicts raise errors instead of last-writer-wins behavior.

Is TLS mandatory? Can I disable it?

TLS is required and cannot be disabled. Self-signed certificates auto-generate for development. Production deployments should provide proper certificates via --cert and --key flags.

What's the difference between standalone and control-plane mode?

Standalone runs everything in one process—simple, perfect for development. Control-plane separates connection handling from DuckDB execution into worker processes, enabling zero-downtime deployments, resource limits, and Kubernetes scaling.

How do I contribute or report issues?

Visit the GitHub repository to open issues, submit PRs, or discuss in GitHub Discussions. The project uses MIT licensing and welcomes contributions.

Can Duckgres replace my production PostgreSQL database?

Not for OLTP workloads. Duckgres excels at analytical queries, but lacks PostgreSQL's full transactional feature set, replication, and extensive ecosystem of extensions. Use it for analytics, reporting, and lakehouse queries—not your primary application database.


Conclusion: The Analytical Database You Already Know How to Use

Duckgres represents a paradigm shift in how we approach analytical databases. Instead of forcing teams to learn new protocols, rewrite applications, or manage separate infrastructure, it delivers DuckDB's insane analytical performance through the PostgreSQL interface you already have.

From local development to multi-tenant Kubernetes deployments, from simple aggregations to lakehouse queries over S3, Duckgres scales with your needs while keeping operational complexity in check. The two-tier query processing is genuinely clever engineering—giving you PostgreSQL compatibility when you want it and DuckDB superpowers when you need them.

PostHog built this for their own production warehouse product. That pedigree shows in every feature: TLS by default, Prometheus metrics, graceful shutdowns, zero-downtime deployments, and comprehensive configuration options.

My take? If you're running analytical workloads against PostgreSQL and feeling the pain, Duckgres isn't just an alternative—it's an upgrade path without the migration trauma. Your existing tools work. Your team doesn't need retraining. Your queries run faster.

Ready to experience it yourself?

⭐ Star the repository, clone it, run just build && just run, and connect with just psql. Your first analytical query will feel like magic.

Get Duckgres on GitHub →

Comments (0)

Comments are moderated before appearing.

No comments yet. Be the first to share your thoughts!

Recommended Prompts

View All
All tools