Browse by type

A PostgreSQL wire protocol compatible server backed by DuckDB. Connect with any PostgreSQL client (psql, pgAdmin, lib/pq, psycopg2, etc.) and get DuckDB's analytical query performance.
COPY FROM STDIN and COPY TO STDOUTDuckgres exposes Prometheus metrics on :9090/metrics. The metrics port is currently fixed at 9090 and cannot be changed via configuration.
See docs/metrics.md for exact request-path boundaries, labels, aggregation rules, PromQL examples, and admission metric migration.
| Metric | Type | Description |
|---|---|---|
duckgres_connections_open |
Gauge | Process-wide number of currently open client connections, including native metadata-proxy sockets |
duckgres_connection_duration_seconds{org} |
Histogram | Worker-backed Duckgres connection lifetime, accept→disconnect (includes _count, _sum, _bucket); excludes native metadata-proxy connections, which use their dedicated duration family |
duckgres_metadata_proxy_connections_open{org} |
Gauge | Current admitted native metadata Postgres proxy connections; process-local, so sum across control-plane replicas |
duckgres_metadata_proxy_connection_attempts_total{org,outcome} |
Counter | Metadata proxy attempts by bounded terminal outcome |
duckgres_metadata_proxy_connection_duration_seconds{org} |
Histogram | Lifetime of admitted metadata proxy connections, including upstream bootstrap |
duckgres_metadata_proxy_upstream_connect_duration_seconds{org,outcome} |
Histogram | Internal metadata Postgres connect/auth latency; outcome is success or error |
duckgres_metadata_proxy_bytes_total{org,direction} |
Counter | Post-authentication pgwire bytes relayed in client_to_upstream or upstream_to_client direction |
duckgres_metadata_proxy_cancel_requests_total{outcome} |
Counter | Raw metadata-proxy CancelRequests handled as session_terminated on the owning control-plane replica or not_local on another replica |
duckgres_query_total{org,status,reason} |
Counter | Total non-empty query attempts. Valid status/reason pairs: success/none; failure/user, failure/canceled, failure/conflict; error/metadata_connection_lost, error/system. |
duckgres_query_duration_seconds{org} |
Histogram | Simple/extended query execution latency (includes _count, _sum, _bucket); use duckgres_query_total for attempt totals |
duckgres_auth_failures_total |
Counter | Process-wide authentication failures, including wrong-password metadata-proxy attempts; use duckgres_metadata_proxy_connection_attempts_total{outcome="auth_failed"} for the proxy-specific split |
duckgres_rate_limit_rejects_total |
Counter | Process-wide pre-TLS connection rejections due to rate limiting; these cannot be attributed to the worker or metadata endpoint because SNI is not available yet |
duckgres_rate_limited_ips |
Gauge | Number of currently rate-limited IP addresses |
duckgres_control_plane_workers_active |
Gauge | Number of active control-plane worker processes |
duckgres_control_plane_worker_acquire_seconds |
Histogram | Time spent acquiring a worker for a new session |
duckgres_control_plane_worker_queue_depth |
Gauge | Approximate number of session requests waiting on worker acquisition |
duckgres_control_plane_worker_spawn_seconds |
Histogram | Time spent spawning and health-checking a new worker |
duckgres_session_admission_evaluation_duration_seconds{decision,reason} |
Histogram | Latency of one DB-backed admission poll for the polling request |
duckgres_session_admission_evaluations_total{decision,reason} |
Counter | Admission request polls; repeated polls are distinct evaluations |
duckgres_session_admission_wait_seconds{org,outcome,reason} |
Histogram | End-to-end wait for one successfully enqueued admission request |
duckgres_session_admission_requests_total{org,outcome,reason} |
Counter | Exactly one terminal event per successfully enqueued admission request |
duckgres_session_admission_queue_depth{org} |
Gauge | Local callers waiting after successful durable enqueue; sum across replicas |
duckgres_session_admission_active_vcpus{org} |
Gauge | Requested vCPUs held by local live lease handles; cleanup-pending durable rows are excluded |
duckgres_session_admission_limit_vcpus{org} |
Gauge | Config-reconciled effective org cap for active org stacks; zero means unlimited, max across replicas |
duckgres_session_admission_reclaim_pending |
Gauge | Activated cleanup intents awaiting or executing exact database reclamation |
duckgres_session_admission_reclaim_attempts_total{outcome} |
Counter | Exact cleanup attempts by success or error outcome |
duckgres_session_admission_reclaim_reservations_in_use |
Gauge | Cleanup-ownership slots held before enqueue, while queued or live, and during pending cleanup |
duckgres_session_admission_reclaim_reservation_capacity |
Gauge | Cleanup-ownership slot capacity for this control-plane process (4096 per reclaimer by default) |
duckgres_session_admission_reclaim_reservation_rejections_total{reason} |
Counter | Reservations rejected because capacity was full, the reclaimer was closed, or the exact reference was a duplicate |
duckgres_session_start_duration_seconds{org,protocol,outcome} |
Histogram | Authenticated PostgreSQL session bootstrap through flushed ReadyForQuery |
duckgres_postgres_session_start_total{org,outcome,reason} |
Counter | Exactly one terminal result per authenticated PostgreSQL session start after server retries; outcome is success\|failure and bounded reasons distinguish operator-actionable failures from client/lifecycle noise |
scripts/test_metrics.sh - Runs a quick sanity check (starts server, runs queries, verifies counts)scripts/load_generator.sh - Generates continuous query load until Ctrl-Cscripts/perf_smoke.sh - Runs the golden-query perf harness and writes artifacts to artifacts/perf/<run_id>scripts/perf_nightly.sh - Nightly wrapper with lock/timeout guards and optional artifact publishermetrics-compose.yml - Starts Prometheus and Grafana locally for metrics (Prometheus at http://localhost:9091, Grafana at http://localhost:3000)When DuckLake uses a Postgres metadata store, Duckgres writes durable per-query
history to the native Postgres table querylog.query_log_entries. The query
log is queryable through ducklake.system.query_log, a live view over that
native Postgres table. The view is not DuckLake snapshot data.
Rows record SQL user (user_name), org, query text, duration, row counts,
errors, trace/span IDs, and profiling-derived resource usage. cpu_time_s is
DuckDB cumulative CPU/thread time in seconds, and peak_buffer_memory_bytes is
DuckDB's system_peak_buffer_memory in bytes, not process RSS.
query_id is a per-statement UUIDv7 minted when the query arrives. It is
time-ordered, appears on the statement's OTEL span (duckgres.query_id) and its
error logs, and is the key that correlates every query-log event for one
statement. A batched simple query (SELECT 1; SELECT 2) runs each statement
under its own query_id, with parent_query_id and statement_index
identifying the Query message they arrived in.
Statements produce a pair of events, using ClickHouse's type vocabulary
(QueryStart = 1, QueryFinish = 2, ExceptionBeforeStart = 3,
ExceptionWhileProcessing = 4):
QueryStart is emitted when the statement begins executing.QueryFinish, or ExceptionWhileProcessing if it
failed after execution began, or ExceptionBeforeStart if it failed before
execution began — auth or policy denial, a transpile error, a failure to
obtain a worker, or an extended-protocol Describe whose prepare the engine
rejected. ExceptionBeforeStart events have no QueryStart, by definition.The boundary is execution began, not an engine saw it: Describe hands
the statement to a worker to learn its result schema, so a binder error there
is an ExceptionBeforeStart even though the engine did see the SQL. This is
the same line ClickHouse draws — analysis-time failures are
ExceptionBeforeStart. In practice this is the largest source of them, so
when triaging, read ExceptionBeforeStart as "never ran", not as "never
reached a worker".
A QueryStart with no terminal event is a query that never came back — a
worker OOM-killed mid-statement, a pod evicted. That row is the only evidence
such a query ever ran, so treat a sustained population of unpaired starts as an
incident signal, allowing for queries still in flight.
The query_id travels to the worker on every statement RPC
(x-duckgres-query-id), and the worker stamps it on its own logs — notably the
"Query appears stuck" warning. That is what closes the loop on an unpaired
QueryStart: the statement's own log row cannot exist, but the pod's last words
about it carry the same ID.
event_time is the statement's start time on every event type, including
terminal ones. This diverges from ClickHouse, where event_time is when the
event was logged: pinning both rows of a pair to the same instant keeps them in
one monthly partition and lets them join without a window function. A terminal
row's finish time is event_time + query_duration_ms.
query_log.start_events selects which statements get a QueryStart:
data (default) — statements that touch data or change schema. Transaction
control, SET/RESET/SHOW, and catalog introspection are skipped: they
never hang, and they are the noisiest statements a driver sends.all — every statement.off — no start events.Terminal events are always logged regardless of this setting, so nothing
disappears from the log; cheap statements simply have no paired start row. Also
settable via DUCKGRES_QUERY_LOG_START_EVENTS.
Each event also records what the statement touches, extracted from its
parse tree (server/querymeta):
access_kinds — the access classes the statement needs, comma-separated:
read, write, ddl, config, admin, transaction, metadata,
unknown. A statement can be several at once: WITH x AS (INSERT …) SELECT
is both a read and a write, which a classifier based on the command tag gets
wrong.query_metadata — JSON with the resolved detail: read_relations and
write_relations (split, because grants are directional), columns,
functions, and table_functions.metadata_complete — false when extraction could not see the whole
statement. DuckDB-native syntax (ATTACH, CREATE SECRET, PIVOT,
SUMMARIZE) is not parseable as PostgreSQL and falls back to a coarse
lexical classification.That last column is load-bearing. These signals exist to let an authorization
policy be evaluated against real traffic before it denies anything, so
"referenced no relations" and "we could not tell what it referenced" must never
be the same answer: a consumer that gates on query_metadata must treat
metadata_complete = false as unknown, and deny.
table_functions is recorded alongside relations because `read_parquet('s3:/
browse all types & interfaces →
$ claude mcp add duckgres \
-- python -m otcore.mcp_server <graph>