pg_exporter is built around a few simple production-oriented principles:
Local-first connectivity: fall back to postgresql:///?sslmode=disable when no explicit URL is provided, which fits same-host deployments
Declarative collection: metric behavior is driven by YAML collector definitions with precise control over ttl, timeout, tags, and fatal
Dynamic planning: choose the appropriate collector branch at runtime based on server version, role, extensions, and tags
Keep serving under failure: use non-blocking startup by default so HTTP endpoints still come up while the database is temporarily unavailable
Hot reload: support POST / GET /reload and SIGHUP reloads, with extra SIGUSR1 support on non-Windows platforms
Split probes from traffic: health endpoints use cached background probes instead of blocking the database on every request
Tighten the management surface: /reload, /explain, and /stat expose runtime and config details, so production deployments should protect them with --web.config.file or keep them internal
Installation
PG Exporter provides multiple installation methods to fit your infrastructure:
docker run -d --name pg_exporter -p 9630:9630 -e PG_EXPORTER_URL="postgres://user:pass@host:5432/postgres" pgsty/pg_exporter:latest
# RPM-based systemssudo tee /etc/yum.repos.d/pigsty-infra.repo > /dev/null <<-'EOF'
[pigsty-infra]
name=Pigsty Infra for $basearch
baseurl=https://repo.pigsty.io/yum/infra/$basearch
enabled = 1
gpgcheck = 0
module_hotfixes=1
EOFsudo yum makecache;sudo yum install -y pg_exporter
sudo tee /etc/apt/sources.list.d/pigsty-infra.list > /dev/null <<EOF
deb [trusted=yes] https://repo.pigsty.io/apt/infra generic main
EOFsudo apt update;sudo apt install -y pg-exporter
# Build from sourcegit clone https://github.com/pgsty/pg_exporter.git
cd pg_exporter
make build
Quick Start
Get PG Exporter up and running in minutes with Getting Started:
# Minimal startup with the local-first default URLpg_exporter
# Or point to a specific targetPG_EXPORTER_URL='postgres://user:pass@localhost:5432/postgres' pg_exporter
# Access metricscurl http://localhost:9630/metrics
# Reload configuration online (POST recommended)curl -X POST http://localhost:9630/reload
Get pg_exporter running and see PostgreSQL metrics in Prometheus within ten minutes
This page is the shortest path: install pg_exporter, connect it to a PostgreSQL instance, verify metrics output, and hook it into Prometheus.
You only need two things: a reachable PostgreSQL 10-19+ (or pgBouncer 1.8+) instance, and permission to create a user in it. For older PostgreSQL 9.1-9.6 instances, see Compatibility.
Step 1: Install
On Linux amd64 you can download the binary directly (for other platforms and RPM/DEB/Docker options, see the Installation guide):
pg_exporter --version
# pg_exporter v1.4.1 (built with go1.26.5 on linux/amd64)
Step 2: Create a Monitoring User
Create a dedicated monitoring user on the target PostgreSQL. The built-in pg_monitor role (PostgreSQL 10+) covers all read permissions the default collectors need:
If you are just trying it out locally as a superuser like postgres, you can skip this step.
Step 3: Run and Verify
Use --dry-run to confirm the configuration parses, then start for real:
exportPG_EXPORTER_URL='postgres://monitor:S3cret@localhost:5432/postgres'pg_exporter --dry-run # print parsed collector config, then exitpg_exporter # start for real, listening on :9630 by default
Without any URL, pg_exporter falls back to the local-first default postgresql:///?sslmode=disable, which fits running on the same host as PostgreSQL. The full URL source precedence (--url > PG_EXPORTER_URL > PGURL > PG_EXPORTER_URL_FILE > default) is documented in the Deployment guide.
pg_up 1 # 1 when the target is reachable, 0 otherwise
pg_version 170000 # version in server_version_num format
pg_in_recovery 0 # 1 on replicas, 0 on primaries
pg_up 1 means the pipeline works — the remaining 600+ metrics (pg_db_*, pg_table_*, pg_wal_*, …) all come from the declarative collector definitions in pg_exporter.yml. If pg_up is 0, restart with pg_exporter --log.level=debug and inspect the connection error.
Collectors cache results per their ttl (most realtime collectors use ttl: 10): as long as the TTL is below the scrape interval, every scrape gets fresh data, while high-frequency scraping can never overwhelm the database. This is also why setting scrape_interval below the common TTLs is not recommended.
That’s it. For Grafana, you can reuse the PostgreSQL dashboards from Pigsty, or explore the live demo.
Troubleshooting
Symptom
What to do
pg_up 0, connection fails
Run pg_exporter --log.level=debug and read the error; check URL, pg_hba.conf, and network reachability
Some metrics are missing
curl localhost:9630/explain to see each collector’s planning verdict (version gates, tags, predicates)
A collector keeps failing
curl localhost:9630/stat for per-collector error counters and durations
Scrapes are slow
Find the slow collector in /stat, raise its ttl, or set skip: true
/stat, /explain, and /reload are management endpoints — protect them with --web.config.file (TLS/auth) or keep them on a trusted network in production. See the API Reference.
Understand and customize collectors (GAUGE/COUNTER/HISTOGRAM, TTL, tags, version gates): Configuration reference
Health check and primary/replica traffic routing endpoints (/up, /primary, /replica): API Reference
2 - Installation
How to download and install the pg_exporter
pg_exporter can be installed via Pigsty, YUM/APT repositories, GitHub release packages (RPM/DEB/Tarball), Docker images, or built from source — pick whichever fits your infrastructure.
Pigsty
The easiest way to get started with pg_exporter is to use Pigsty,
which is a complete PostgreSQL distribution with built-in Observability best practices based on pg_exporter, Prometheus, and Grafana.
You don’t even need to know any details about pg_exporter; it just gives you all the metrics and dashboard panels.
You can install it directly with your OS package manager (rpm/dpkg), or just place the binary in your $PATH. Current tarballs also include pg_exporter.yml, package/pg_exporter.default, package/pg_exporter.service, and LICENSE for manual deployments.
RPM package rename
Starting with v1.4.1, the official RPM package name and artifact prefix change from pg_exporter to pg-exporter, matching the DEB package and repository install commands. The new RPM also provides and obsoletes the legacy pg_exporter package name, allowing direct upgrades from earlier releases.
Full SHA256 checksums are available in checksums.txt on the release page; version-specific checksums are also archived in the release notes.
Repository
The pg_exporter package is also available in the pigsty-infra repo.
You can add the repo to your system and install it with your OS package manager:
YUM
For EL distributions such as RHEL, Rocky Linux, CentOS, AlmaLinux, and Oracle Linux:
sudo tee /etc/yum.repos.d/pigsty-infra.repo > /dev/null <<-'EOF'
[pigsty-infra]
name=Pigsty Infra for $basearch
baseurl=https://repo.pigsty.io/yum/infra/$basearch
enabled = 1
gpgcheck = 0
module_hotfixes=1
EOFsudo yum makecache;sudo yum install -y pg_exporter
APT
For Debian, Ubuntu and compatible Linux Distributions:
sudo tee /etc/apt/sources.list.d/pigsty-infra.list > /dev/null <<EOF
deb [trusted=yes] https://repo.pigsty.io/apt/infra generic main
EOFsudo apt update;sudo apt install -y pg-exporter
Docker
We have prebuilt docker images for amd64 and arm64 architectures on docker hub: pgsty/pg_exporter.
The current Docker image is built from scratch. If you connect to remote PostgreSQL with sslmode=verify-ca or verify-full, mount an explicit CA certificate (sslrootcert or a system CA bundle), otherwise TLS verification may fail.
Compatibility
The default configuration supports PostgreSQL 10-19+. For EOL PostgreSQL versions, use the bundled legacy/ config package for compatible monitoring.
PostgreSQL Version
Support Status
10 ~ 19+
✅ Full Support (default config)
9.1 ~ 9.6
⚠️ Use legacy/pg_exporter.yml
9.0 and earlier
❌ Unsupported
Legacy config example:
make conf9
PG_EXPORTER_CONFIG=legacy/pg_exporter.yml pg_exporter
pg_exporter works with pgBouncer 1.8+, since v1.8 is the first version with SHOW command support.
pgBouncer Version
Support Status
1.8.x ~ 1.25+
✅ Full Support
before 1.8.x
⚠️ No Metrics
3 - Configuration
Every business metric in pg_exporter is driven by a YAML collector definition: one SQL query plus its execution conditions (version, role, tags, predicates) and runtime controls (caching, timeout). This page is the complete reference for collector definitions.
A configuration can be a single YAML file (like the default pg_exporter.yml) or a directory of YAML files — the official default bundle is merged from the 58 definition files under config/.
Configuration Loading
PG Exporter searches for configuration in the following order:
Only .yml / .yaml files in that directory are loaded, non-recursively
Files are merged in lexicographic order; later files override earlier collector definitions with the same top-level name
If a config directory contains YAML files but every one of them fails to parse, the exporter returns an error instead of silently ignoring the directory
Collector Structure
Each collector is a top-level object in the YAML configuration with a unique name and various properties:
collector_branch_name:# Unique identifier for this collectorname:metric_namespace # Metric prefix (defaults to branch name)desc:"Collector description"# Human-readable descriptionquery:| # SQL query to executeSELECT column1, column2FROM table# Execution Controlttl:10# Cache time-to-live in secondstimeout:0.1# Query timeout in secondsfatal:false# If true, failure fails entire scrapeskip:false# If true, collector is disabled# Version Compatibilitymin_version:100000# Minimum PostgreSQL version (inclusive)max_version:999999# Maximum PostgreSQL version (exclusive)# Execution Tagstags:[cluster, primary] # Conditions for execution# Predicate Queries (optional)predicate_queries:- name:"check_function"predicate_query:| SELECT EXISTS (...)# Metric Definitionsmetrics:- column_name:usage:GAUGE # GAUGE, COUNTER, HISTOGRAM, LABEL, or DISCARDrename: metric_name # Optional:rename the metricdescription:"Help text"# Metric descriptiondefault:0# Default value if NULLscale:1000# Scale factor for the valuebucket:[1,10,100]# Bucket upper bounds for HISTOGRAM columns (strictly increasing, +Inf appended)
Validation rules:
Each entry in metrics must define exactly one column mapping
Each collector must expose at least one GAUGE, COUNTER, or HISTOGRAM column
usage only accepts GAUGE, COUNTER, HISTOGRAM, LABEL, or DISCARD
HISTOGRAM columns must define bucket: a finite, strictly increasing list of bucket upper bounds; the +Inf bucket is appended automatically
Metric names and label names are validated against Prometheus naming rules during load; invalid configs fail fast
Constant labels are checked for conflicts during load; they cannot overlap with query labels or built-in dynamic labels such as datname and query; when any HISTOGRAM collector is configured, le is reserved and cannot be used as a constant label
The SQL result must include every column declared as LABEL; since v1.4.1, a missing label column fails that collector’s entire scrape instead of emitting an empty label or retaining stale results, while other non-fatal collectors continue normally
If you use one-line inline metrics definitions, keep description values double-quoted to avoid YAML ambiguity
Core Configuration Elements
Collector Branch Name
The top-level key uniquely identifies a collector across the entire configuration:
pg_stat_database:# Must be uniquename:pg_db # Actual metric namespace
Query Definition
The SQL query that retrieves metrics:
query:| SELECT
datname,
numbackends,
xact_commit,
xact_rollback,
blks_read,
blks_hit
FROM pg_stat_database
WHERE datname NOT IN ('template0', 'template1')
Metric Types
Each column in the query result must be mapped to a metric type:
Usage
Description
Example
GAUGE
Instantaneous value that can go up or down
Current connections
COUNTER
Cumulative value that only increases
Total transactions
HISTOGRAM
Snapshot histogram deriving _bucket / _count / _sum series
Transaction age distribution
LABEL
Use as a Prometheus label
Database name
DISCARD
Ignore this column
Internal values
Histogram Columns (HISTOGRAM)
v1.4.0 introduces the HISTOGRAM column type: every row returned by the query counts
as one observation, aggregated per label group into a classic Prometheus histogram
snapshot, deriving three series families: <name>_bucket (with the le label and the
+Inf bucket), <name>_count, and <name>_sum:
pg_xact_age:name:pg_xact_agedesc:"Open transaction age distribution histogram"query:| SELECT datname,
greatest(0, extract(epoch FROM now() - xact_start)) AS seconds
FROM pg_stat_activity
WHERE pid <> pg_backend_pid() AND backend_type = 'client backend'
AND datname IS NOT NULL AND xact_start IS NOT NULL;ttl:10tags:[cluster]metrics:- datname:{usage: LABEL, description:"Database name"}- seconds:usage:HISTOGRAMbucket:[1,3,10,30,100,300,1000,3000,10000,30000,100000]description:"Open transaction age snapshot in seconds"
Usage notes:
This is a snapshot histogram: the whole distribution is rebuilt on every scrape,
so bucket counts can go up or down — gauge-like semantics. histogram_quantile()
works directly, but rate() / increase() over _count / _sum is meaningless
SQL NULL observations are ignored by default; with an explicit default, they
count as the default value
scale is applied to the observation before bucket assignment; as with scalar
columns, timestamp and boolean values are exempt from scale
The pg_xact_age
collector in the default bundle serves as the reference implementation
Cache Control (TTL)
The ttl parameter controls result caching:
# Fast queries - minimal cachingpg_stat_activity:ttl:1# Cache for 1 second# Expensive queries - longer cachingpg_table_bloat:ttl:3600# Cache for 1 hour
Best practices:
Set TTL less than your scrape interval
Use longer TTL for expensive queries
TTL of 0 disables caching
Timeout Control
Prevent queries from running too long:
timeout:0.1# 100ms defaulttimeout:1.0# 1 second for complex queriestimeout:-1# Disable timeout (not recommended)
Version Compatibility
Control which PostgreSQL versions can run this collector:
Version numbers follow PostgreSQL server_version_num rules:
100000 = 10.0
130200 = 13.2
160100 = 16.1
190000 = 19.0
90600 = 9.6, relevant when using the legacy config bundle
Execution Model
Understanding the full path from a collector definition to emitted metrics helps answer “why is this metric missing”:
Planning (on connection setup or hot reload): each collector branch is checked in turn — target type (PostgreSQL / pgBouncer), min_version / max_version gates, tags matching against server role and exporter tags, and the skip switch. Branches that fail any check are not installed on that target. curl localhost:9630/explain shows exactly the verdict of this step.
Scraping (on every /metrics request): for each installed collector — if the cache is still within ttl, the cached result is returned; otherwise predicate_queries run first (any false verdict skips this round and bumps pg_exporter_query_scrape_predicate_skip_count), then the main query executes under timeout, and results are converted to metrics and cached.
Failure semantics: a normal collector failure only affects itself (pg_exporter_query_scrape_error_count goes up, its metric group is absent this round); a failing collector marked fatal: true fails the whole server scrape.
Tag System
Tags control when and where collectors execute:
Built-in Tags
Tag
Description
cluster
Execute once per PostgreSQL cluster
primary / master
Only on primary servers
standby / replica
Only on replica servers
pgbouncer
Only for pgBouncer connections
Prefixed Tags
Prefix
Example
Description
dbname:
dbname:postgres
Only on specific database
username:
username:monitor
Only with specific user
extension:
extension:pg_stat_statements
Only if extension installed
schema:
schema:public
Only if schema exists
not:
not:slow
NOT when exporter has tag
Custom Tags
Pass custom tags to the exporter:
pg_exporter --tag="production,critical"
Then use in configuration:
expensive_metrics:tags:[critical] # Only runs with 'critical' tag
Predicate Queries
Execute conditional checks before main query:
predicate_queries:- name:"Check pg_stat_statements"predicate_query:| SELECT EXISTS (
SELECT 1 FROM pg_extension
WHERE extname = 'pg_stat_statements'
)
The main query only executes if all predicates return true.
Metric Definition
Basic Definition
metrics:- numbackends:usage:GAUGEdescription:"Number of backends connected"
Advanced Options
metrics:- checkpoint_write_time:usage:COUNTERrename:write_time # Rename metricscale:0.001# Convert ms to secondsdefault:0# Use 0 if NULLdescription:"Checkpoint write time in seconds"
Collector Organization
PG Exporter ships with pre-organized collectors:
Range
Category
Description
0xx
Documentation
Examples and documentation
1xx
Basic
Server info, settings, metadata
2xx
Replication
Replication, slots, receivers
3xx
Persistence
I/O, checkpoints, WAL
4xx
Activity
Connections, locks, queries
5xx
Progress
Vacuum, index creation progress
6xx
Database
Per-database statistics
7xx
Objects
Tables, indexes, functions
8xx
Optional
Expensive/optional metrics
9xx
pgBouncer
Connection pooler metrics
10xx+
Extensions
Extension-specific metrics
Real-World Examples
Simple Gauge Collector
pg_connections:desc:"Current database connections"query:| SELECT
count(*) as total,
count(*) FILTER (WHERE state = 'active') as active,
count(*) FILTER (WHERE state = 'idle') as idle,
count(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction
FROM pg_stat_activity
WHERE pid != pg_backend_pid()ttl:1metrics:- total:{usage: GAUGE, description:"Total connections"}- active:{usage: GAUGE, description:"Active connections"}- idle:{usage: GAUGE, description:"Idle connections"}- idle_in_transaction:{usage: GAUGE, description:"Idle in transaction"}
pg_stat_statements_metrics:desc:"Query performance statistics"tags:[extension:pg_stat_statements]query:| SELECT
sum(calls) as total_calls,
sum(total_exec_time) as total_time,
sum(mean_exec_time * calls) / sum(calls) as mean_time
FROM pg_stat_statementsttl:60metrics:- total_calls:{usage:COUNTER}- total_time:{usage: COUNTER, scale:0.001}- mean_time:{usage: GAUGE, scale:0.001}
Custom Collectors
Creating Your Own Metrics
Create a new YAML file in your config directory:
# /etc/pg_exporter/custom_metrics.ymlapp_metrics:desc:"Application-specific metrics"query:| SELECT
(SELECT count(*) FROM users WHERE active = true) as active_users,
(SELECT count(*) FROM orders WHERE created_at > NOW() - '1 hour'::interval) as recent_orders,
(SELECT avg(processing_time) FROM jobs WHERE completed_at > NOW() - '5 minutes'::interval) as avg_job_timettl:30metrics:- active_users:{usage: GAUGE, description:"Currently active users"}- recent_orders:{usage: GAUGE, description:"Orders in last hour"}- avg_job_time:{usage: GAUGE, description:"Average job processing time"}
Test your collector:
pg_exporter --explain --config=/etc/pg_exporter/
Conditional Metrics
Use predicate queries for conditional metrics:
partition_metrics:desc:"Partitioned table metrics"predicate_queries:- name:"Check if partitioning is used"predicate_query:| SELECT EXISTS (
SELECT 1 FROM pg_class
WHERE relkind = 'p' LIMIT 1
)query:| SELECT
parent.relname as parent_table,
count(*) as partition_count,
sum(pg_relation_size(child.oid)) as total_size
FROM pg_inherits
JOIN pg_class parent ON parent.oid = pg_inherits.inhparent
JOIN pg_class child ON child.oid = pg_inherits.inhrelid
WHERE parent.relkind = 'p'
GROUP BY parent.relnamettl:300metrics:- parent_table:{usage:LABEL}- partition_count:{usage:GAUGE}- total_size:{usage:GAUGE}
Performance Optimization
Query Optimization Tips
Use appropriate TTL values:
Fast queries: 1-10 seconds
Medium queries: 10-60 seconds
Expensive queries: 300-3600 seconds
Set realistic timeouts:
Default: 100ms
Complex queries: 500ms-1s
Never disable timeout in production
Use cluster-level tags:
tags:[cluster] # Run once per cluster, not per database
Disable expensive collectors:
pg_table_bloat:skip:true# Disable if not needed
Monitoring Collector Performance
Check collector execution statistics:
# View collector statistics (hit / error / skip counters and durations)curl http://localhost:9630/stat
# Per-collector duration and error counters, by datname/querycurl -s http://localhost:9630/metrics | grep -E 'pg_exporter_query_scrape_(duration|error_count)'
pg_exporter exposes four kinds of HTTP endpoints on its listen port (default :9630): metrics, health checks, traffic routing, and operational management. The full endpoint list:
Health and routing endpoints answer from a cached background-probe role state (primary / replica / down / starting / unknown) — they never query the database synchronously per HTTP request, so probe storms cannot reach the database.
Metrics Endpoint
GET /metrics
The primary endpoint that exposes all collected metrics in Prometheus format.
Request
curl http://localhost:9630/metrics
Response
# HELP pg_up last scrape was able to connect to the server: 1 for yes, 0 for no
# TYPE pg_up gauge
pg_up 1
# HELP pg_version server version number
# TYPE pg_version gauge
pg_version 140000
# HELP pg_in_recovery server is in recovery mode? 1 for yes 0 for no
# TYPE pg_in_recovery gauge
pg_in_recovery 0
# HELP pg_exporter_build_info A metric with a constant '1' value labeled with version, revision, branch, goversion, builddate, goos, and goarch from which pg_exporter was built.
# TYPE pg_exporter_build_info gauge
pg_exporter_build_info{version="v1.4.1",branch="main",revision="<git-sha>",builddate="<build-date>",goversion="go1.26.5",goos="linux",goarch="amd64"} 1
# ... additional metrics
Response Format
Metrics follow the Prometheus exposition format:
# HELP <metric_name> <description>
# TYPE <metric_name> <type>
<metric_name>{<label_name>="<label_value>",...} <value> <timestamp>
Self-Monitoring Metrics
Besides business metrics defined by YAML collectors, /metrics also exposes the exporter’s own runtime metrics (disable the pg_exporter_* part with --disable-intro; the prefix follows --namespace and becomes pgbouncer_ in pgBouncer mode):
Metric
Labels
Description
pg_up
—
1 when the target database is reachable, 0 otherwise
pg_version
—
Server version in server_version_num format
pg_in_recovery
—
1 when in recovery mode (replica)
pg_exporter_build_info
version, revision, …
Constant 1 with build info in labels
pg_exporter_up
—
Constant 1 while the exporter is alive
pg_exporter_uptime
—
Seconds since the exporter started
pg_exporter_scrape_total_count / _error_count
—
Cumulative scrape / failure counts
pg_exporter_scrape_duration
—
Duration of the last scrape in seconds
pg_exporter_last_scrape_time
—
Timestamp of the last scrape
pg_exporter_server_scrape_*
datname
Per-database scrape duration and success/failure counters
Note that both share the same semantics: they return 503 when the target database is unreachable. If you don’t want “database down” to restart the exporter Pod, use a TCP probe on the listen port for liveness instead.
Traffic Routing
These endpoints are designed for load balancers and proxies to route traffic based on server role.
GET /primary
Check whether the server is a primary instance.
Response Codes
Code
Status
Description
200
OK
Server is primary and accepting writes
404
Not Found
Server is not primary and is acting as a replica
503
Service Unavailable
Server is unavailable (down / starting / unknown)
Aliases
/leader
/master
/read-write
/rw
Example
# Check whether the server is primarycurl -I http://localhost:9630/primary
# Use in HAProxybackend pg_primary
option httpchk GET /primary
server pg1 10.0.0.1:5432 check port 9630 server pg2 10.0.0.2:5432 check port 9630
GET /replica
Check whether the server is a replica instance.
Response Codes
Code
Status
Description
200
OK
Server is a replica and in recovery
404
Not Found
Server is not a replica and is acting as primary
503
Service Unavailable
Server is unavailable (down / starting / unknown)
Aliases
/standby
/read-only
/ro
/slave remains compatible, but /replica is the preferred name.
Example
# Check whether the server is a replicacurl -I http://localhost:9630/replica
# Use in a load balancerbackend pg_replicas
option httpchk GET /replica
server pg2 10.0.0.2:5432 check port 9630 server pg3 10.0.0.3:5432 check port 9630
GET /read
Check whether the server can handle read traffic. Both primaries and replicas may return success.
Response Codes
Code
Status
Description
200
OK
Server is healthy and can handle reads
503
Service Unavailable
Server is unavailable (down / starting / unknown)
Example
# Check whether the server can serve readscurl -I http://localhost:9630/read
# Route reads to any healthy serverbackend pg_read
option httpchk GET /read
server pg1 10.0.0.1:5432 check port 9630 server pg2 10.0.0.2:5432 check port 9630 server pg3 10.0.0.3:5432 check port 9630
Operational Endpoints
GET /reload / POST /reload
Reload configuration without restarting the exporter.
Request
# POST is recommendedcurl -X POST http://localhost:9630/reload
# GET remains supported for compatibilitycurl http://localhost:9630/reload
Response
server reloaded
Response Codes
Code
Status
Description
200
OK
Reload completed successfully
500
Internal Server Error
Reload failed and returns fail to reload: ...
405
Method Not Allowed
Non-GET/POST request, with Allow: GET, POST
Use Cases
Update collector definitions
Change query parameters
Modify cache TTL values
Add or remove collectors
Note
Reload refreshes collector configuration and query plans. Process-level settings such as listen addresses and CLI arguments still require a restart.
Security Advice
/reload, /explain, and /stat are management endpoints. If the exporter is reachable beyond localhost or a trusted private network, protect them with --web.config.file or restrict access at the reverse proxy or firewall layer.
GET /explain
Display planned collector execution details for all configured collectors.
This endpoint is useful when identifying slow or problematic collectors.
Using with Load Balancers
HAProxy Example
# Primary backend for write traffic
backend pg_primary
mode tcp
option httpchk GET /primary
http-check expect status 200
server pg1 10.0.0.1:5432 check port 9630 inter 3000 fall 2 rise 2
server pg2 10.0.0.2:5432 check port 9630 inter 3000 fall 2 rise 2 backup
# Replica backend for read traffic
backend pg_replicas
mode tcp
balance roundrobin
option httpchk GET /replica
http-check expect status 200
server pg2 10.0.0.2:5432 check port 9630 inter 3000 fall 2 rise 2
server pg3 10.0.0.3:5432 check port 9630 inter 3000 fall 2 rise 2
# Read backend for any server that can handle reads
backend pg_read
mode tcp
balance leastconn
option httpchk GET /read
http-check expect status 200
server pg1 10.0.0.1:5432 check port 9630 inter 3000 fall 2 rise 2
server pg2 10.0.0.2:5432 check port 9630 inter 3000 fall 2 rise 2
server pg3 10.0.0.3:5432 check port 9630 inter 3000 fall 2 rise 2
A Note on Nginx
Open-source Nginx does not support active out-of-band HTTP health checks (the health_check directive is an NGINX Plus feature), and PostgreSQL traffic requires the stream module rather than http proxying. For role-based PostgreSQL traffic routing, prefer HAProxy as shown above, or solutions like Patroni + vip-manager.
5 - Deployment
Production deployment — connection & credentials, systemd / Docker / Kubernetes, auto-discovery and alerting
This page covers what it takes to run pg_exporter in production: process arguments and environment variables, monitoring user and credential management, the systemd / Docker / Kubernetes deployment forms, pgBouncer and auto-discovery, plus scrape and alerting configuration on the Prometheus side.
Process-level configuration comes from two sources, in decreasing precedence:
Command-line arguments (--url, --config, …)
Environment variables (every flag has a corresponding PG_EXPORTER_* variable)
Metric collection behavior is entirely driven by YAML collector definitions (default /etc/pg_exporter.yml, or a config directory) — see the Configuration reference.
Flags:
-h, --[no-]help Show context-sensitive help(also try --help-long and --help-man).
-u, --url=URL postgres target url
-c, --config=CONFIG path to config dir or file
--web.listen-address=:9630 ...
Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. Examples: `:9100` or `[::1]:9100`for http, `vsock://:9100`for vsock
--web.config.file="" Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md
-l, --label="" constant labels: comma separated list of label=value pair ($PG_EXPORTER_LABEL) -t, --tag="" tags, comma separated list of server tag ($PG_EXPORTER_TAG) -C, --[no-]disable-cache force not using cache ($PG_EXPORTER_DISABLE_CACHE) -m, --[no-]disable-intro disable internal/exporter self-monitoring metrics (only expose query metrics)($PG_EXPORTER_DISABLE_INTRO) -a, --[no-]auto-discovery automatically scrape all databases on the target server ($PG_EXPORTER_AUTO_DISCOVERY) -x, --exclude-database="template0,template1,postgres" excluded databases when auto-discovery is enabled ($PG_EXPORTER_EXCLUDE_DATABASE) -i, --include-database="" included databases when auto-discovery is enabled ($PG_EXPORTER_INCLUDE_DATABASE) -n, --namespace="" prefix of built-in metrics, (pg|pgbouncer) by default ($PG_EXPORTER_NAMESPACE) -f, --[no-]fail-fast fail fast instead of waiting during start-up ($PG_EXPORTER_FAIL_FAST) -T, --connect-timeout=100 connect timeout in ms, 100 by default ($PG_EXPORTER_CONNECT_TIMEOUT) -P, --web.telemetry-path="/metrics" URL path under which to expose metrics ($PG_EXPORTER_TELEMETRY_PATH) -D, --[no-]dry-run dry run and print raw configs
-E, --[no-]explain explain server planned queries
--log.level="info" log level: debug|info|warn|error
--log.format="logfmt" log format: logfmt|json
--[no-]version Show application version.
Two deployment-relevant behaviors worth knowing:
Startup policy: non-blocking startup is the default — when the target database is temporarily unreachable, the HTTP endpoints come up anyway and a background probe keeps retrying until recovery. If you prefer “fail on unreachable” (e.g. delegating restart decisions to systemd or an orchestrator), set --fail-fast.
Telemetry path validation: since v1.4.0, --web.telemetry-path is strictly validated at startup — empty paths, conflicts with built-in endpoints, or non-canonical paths like //metrics that could never match fail immediately with a clear error.
Connection URL Sources
The connection string resolves from the following sources, first non-empty value wins:
--url / -u command-line argument
PG_EXPORTER_URL environment variable
PGURL environment variable
Content of the file pointed to by PG_EXPORTER_URL_FILE (fits container Secret mounts)
When the URL omits sslmode, sslmode=disable is appended automatically. Also, libpq service-file environment variables (PGSERVICE / PGSERVICEFILE, etc.) are cleared at startup with a log line — service files could override the explicit connection target, and pg_exporter guarantees that the URL announced in logs is the URL actually connected to.
Monitoring User and Credentials
Create the Monitoring User
CREATEROLEmonitorWITHLOGINPASSWORD'S3cret'CONNECTIONLIMIT5;GRANTpg_monitorTOmonitor;-- built-in monitoring role (PostgreSQL 10+), covers all default collectors
Keeping CONNECTION LIMIT is recommended: the exporter normally holds only one to a few connections (one per database with auto-discovery), and the limit prevents connection exhaustion on misconfiguration.
Manage Passwords with .pgpass
Take the password out of the URL and let libpq’s .pgpass provide it:
# Create as the OS user that runs the exporterecho"localhost:5432:*:monitor:S3cret" > ~/.pgpass
chmod 600 ~/.pgpass
# URL without passwordPG_EXPORTER_URL='postgres://monitor@localhost:5432/postgres'
RPM/DEB package installs
Packaged services run as the prometheus system user. Since v1.4.0 its HOME points to /var/lib/prometheus (where libpq looks up ~/.pgpass), but the package does not create that directory. Before using .pgpass, run:
install -d -o prometheus -g prometheus /var/lib/prometheus
Beyond /metrics, the /reload, /explain, and /stat management endpoints let anyone with port access read configuration and runtime state, or trigger reloads. If the exporter is reachable from a shared network, enable TLS / Basic Auth via --web.config.file (exporter-toolkit web configuration), or restrict access at the firewall / reverse-proxy layer.
Systemd Deployment (RPM/DEB packages)
The RPM/DEB packages ship a service unit and an environment file; after installation, edit the environment file and start:
# /usr/lib/systemd/system/pg_exporter.service[Unit]Description=Prometheus exporter for PostgreSQL/Pgbouncer server metricsDocumentation=https://pigsty.io/docs/pg_exporterAfter=network.target[Service]EnvironmentFile=-/etc/default/pg_exporterUser=prometheusExecStart=/usr/bin/pg_exporter $PG_EXPORTER_OPTSRestart=on-failure[Install]WantedBy=multi-user.target
Every command-line flag has a corresponding environment variable — append what you need here (e.g. PG_EXPORTER_DISABLE_INTRO). The file is packaged as noreplace, so upgrades never overwrite your edits.
Common operations:
sudo systemctl enable --now pg_exporter # start and enable at bootsudo systemctl status pg_exporter # check statusjournalctl -u pg_exporter -f # follow logscurl -X POST localhost:9630/reload # hot-reload collector config (no restart)
The official image is built from scratch and contains no system CA certificates. When connecting to remote PostgreSQL with sslmode=verify-ca / verify-full, mount a CA certificate explicitly and point sslrootcert at it, or TLS verification cannot complete.
You can also use PG_EXPORTER_URL_FILE pointing at a Secret-mounted file, keeping the connection string out of environment variables.
Auto-Discovery
Auto-discovery (enabled by default) lets one exporter instance monitor every database in the target PostgreSQL:
pg_exporter --auto-discovery \
--exclude-database="template0,template1,postgres"\ # default exclusion list --include-database=""# set to switch to allowlist mode
Behavior:
Cluster-level collectors (tags: [cluster]) run once on the primary connection
Database-level collectors run on every discovered database, with metrics distinguished by the datname label
Newly created / dropped databases are picked up / removed in subsequent planning cycles
Monitoring pgBouncer
Set the database name in the URL to pgbouncer to switch to pgBouncer mode (this is what triggers the detection):
In pgBouncer mode, the exporter uses the pgbouncer metric prefix and runs only pgBouncer-specific collectors (SHOW STATS / SHOW POOLS, etc.). The usual pattern is one exporter instance for PostgreSQL and another for pgBouncer, on different ports.
Keep the scrape interval at or above the common collector ttl (mostly 10 seconds in the default bundle): TTL caching means more frequent scrapes would only receive cached results anyway.
Alert Rules
All rules below are based on metrics that actually exist:
groups:- name:pg_exporterrules:# Exporter process unreachable- alert:PgExporterDownexpr:up{job="postgresql"} == 0for:1mlabels:{severity:critical }annotations:summary:"pg_exporter down ({{ $labels.instance }})"# Exporter alive but cannot reach the database- alert:PostgreSQLDownexpr:pg_up == 0for:1mlabels:{severity:critical }annotations:summary:"PostgreSQL connection failed ({{ $labels.instance }})"# Overall scrape duration abnormal (unit: seconds)- alert:PgExporterSlowScrapeexpr:pg_exporter_scrape_duration > 10for:5mlabels:{severity:warning }annotations:summary:"pg_exporter slow scrape ({{ $labels.instance }})"# A specific collector keeps failing (locate by datname/query)- alert:PgExporterQueryErrorexpr:increase(pg_exporter_query_scrape_error_count[10m]) > 0for:10mlabels:{severity:warning }annotations:summary:"Collector {{ $labels.query }} keeps failing on {{ $labels.datname }}"
Role-Based Traffic Routing
The /primary, /replica, and /read health-check endpoints can serve directly as health probes for HAProxy and similar load balancers, enabling primary/replica read-write splitting. Endpoint semantics and a complete HAProxy example are in the API Reference.
Note: open-source Nginx does not support active out-of-band HTTP health checks (health_check is an NGINX Plus feature). For role-based PostgreSQL traffic routing, prefer HAProxy, or solutions like Patroni + vip-manager.
6 - Release Notes
The latest stable version of pg_exporter is v1.4.1
v1.4.1 is a maintenance release focused on metric accuracy and RPM upgrade compatibility.
Highlights:
Fix pg_subrel_count overcounting when parallel logical replication apply workers are active: deduplicate pg_stat_subscription by subscription ID and name before aggregating subscription relation states
If a query result omits a configured LABEL column, that collector’s scrape now fails atomically instead of emitting an empty label or retaining stale results; other non-fatal collectors continue normally
Rename the official RPM package and artifact prefix from pg_exporter to pg-exporter; the new package provides and obsoletes the legacy name, allowing direct upgrades
Refresh version and standalone package metadata, add missing-label regression coverage, and validate RPM configuration, artifact names, and compatibility metadata in CI
Upgrade Notes:
Custom collector SQL must return every LABEL column; the result schema must remain complete even when the query returns zero rows
If automation matches GitHub RPM file names directly, update pg_exporter-*.rpm to pg-exporter-*.rpm
v1.4.0 introduces the Snapshot Histogram metric type and the new pg_xact_age transaction age collector, along with a round of systematic hardening across HTTP routing, packaging, and the build toolchain.
New Features:
New HISTOGRAM column type: SQL query snapshots can be aggregated per label group into classic Prometheus histograms, deriving _bucket / _count / _sum series families; bucket bounds are strictly validated at config load time (finite, strictly increasing, +Inf appended automatically), le becomes a reserved label, and hot reload is fully supported
New pg_xact_age collector: exposes the distribution of open transaction age (pg_xact_age_seconds) and idle-in-transaction age (pg_xact_age_idle_seconds) as histograms; cluster-level collection, client backends only, 10s TTL
Default config bundle grows from 57 to 58 definition files; pg_xact_age takes slot 0450, with pg_lock / pg_lock_stat / pg_query renumbered to 0460 / 0470 / 0480 (contents unchanged)
Fixes & Improvements:
HTTP route registration is isolated to a private ServeMux instead of the global DefaultServeMux, so endpoints registered by third-party libraries can no longer be exposed accidentally
--web.telemetry-path is strictly validated at startup: empty paths, paths not starting with /, paths containing ?#{}, conflicts with built-in endpoints, and non-canonical paths like //metrics that could never be matched all fail fast with a clear error
The landing page HTML-escapes the telemetry path
The primary connection pool is properly closed when --fail-fast pre-check fails
RPM / DEB packaging fixes (#105): the prometheus system user’s HOME now points to /var/lib/prometheus (where libpq looks for ~/.pgpass), and the packaged default URL gains the /postgres database name so libpq no longer falls back to a database named after the OS user
Version string unified with the v prefix: --version output, the /version endpoint, and the pg_exporter_build_info{version=...} label of official release binaries now report v1.4.0 (GoReleaser artifacts previously reported unprefixed 1.3.0); artifact file names, package versions, and Docker image tag conventions are unchanged
Histogram value casting aligned with the scalar path: timestamp and boolean columns are exempt from scale
GoReleaser embedded package metadata corrected: supported range updated to PostgreSQL 9.x - 19+ and pgBouncer 1.8 - 1.25+
make docker restores GOPROXY / GOSUMDB build-arg passthrough
Engineering & Build:
Build toolchain updated to Go 1.26.5, exporter-toolkit v0.17.1, and prometheus/common v0.70.0
New regular CI verification workflow: module tidy check, generated-config drift check (make conf output must stay in sync with config/*.yml), race tests, and six-platform cross-builds
Docker build tooling consolidated: the docker/ scripts and make docker-release are removed; multi-arch release images are built by GoReleaser
Config coverage tests generalized: tests no longer assume specific collectors exist in the config directory, making trimmed custom bundles easier to maintain
Upgrade Notes:
If you were running with a non-canonical telemetry path (e.g. //metrics), the process will now refuse to start — previously it started but the metrics endpoint was silently unreachable
Automation that parses the version label of pg_exporter_build_info or the --version output needs to accommodate the v prefix
When any HISTOGRAM collector is configured, le can no longer be used as a constant label
v1.3.0 adds PostgreSQL 19 support, and refreshes the default collector bundle, build toolchain, and config coverage tests.
Highlights:
Default support range extended from PostgreSQL 10-18+ to 10-19+
Default config bundle includes 57config/*.yml definition files
Build dependencies updated to Go 1.26.4, lib/pq v1.12.3, Prometheus client v1.23.2, and exporter-toolkit v0.16.0
pg_recovery_state: collect pg_stat_recovery on recovery nodes, exposing promotion trigger status, replay LSN, timeline, recovery transaction time, and pause state
pg_lock_stat: collect PG19 pg_stat_lock, exposing waits, wait time, and fast-path overflow counts by locktype
pg_vacuum_score: collect the current-database summary from pg_stat_autovacuum_scores, exposing max autovacuum score and candidate table count
pg_wal_19: expose wal_fpi_bytes on PG19 as pg_wal_fpi_bytes
pg_sub_19: adapt sequence sync and logical replication conflict stats from pg_stat_subscription_stats, while keeping sync_error_count for legacy dashboards
pg_recv: recognize the PG13+ WAL receiver connecting state on PG19
pg_slot: recognize the PG19 replication slot invalidation reason idle_timeout
pg_db_confl: switch to an explicit column list to avoid accidentally exporting future view columns
pg_backup, pg_vacuuming, and pg_clustering continue reusing stable existing branches instead of changing the metric surface for low-value PG19 fields
v1.2.2 is a routine maintenance release that only refreshes the release toolchain to Go 1.26.2. It does not introduce new collectors, config semantics, or runtime behavior changes.
Highlights
Refresh the release toolchain: bump release builds to Go 1.26.2
No functional changes: collector behavior, default configs, metric definitions, and runtime semantics remain unchanged
v1.2.1 is a lightweight maintenance release focused on release engineering, config package consistency, and documentation/metadata refresh. It does not introduce new collector semantics or runtime behavior changes.
Highlights
Refresh the build toolchain: bump both release workflows and Docker build images to Go 1.26.1
Standardize config style: switch inline description values in both current and legacy configs to double-quoted form, and regenerate merged pg_exporter.yml / legacy/pg_exporter.yml
Add config consistency tests: verify split and merged configs remain equivalent, and check inline metric description style to reduce configuration drift
Refresh packaging metadata: update RPM / DEB support descriptions to PostgreSQL 9.x - 18+ and pgBouncer 1.8 - 1.25+, and refresh Pigsty documentation links
v1.2.0 is a stability-and-compatibility focused minor release across startup flow, hot reload, health probing, config validation, and legacy support.
New Features:
Add robust hot reload workflow: support platform-specific reload signals (SIGHUP / SIGUSR1) and strengthen POST /reload to refresh configs and query plans without process restart
Switch startup to non-blocking mode: HTTP endpoints come up first even when target precheck fails, making recovery and monitoring integration smoother
Add PostgreSQL 9.1-9.6 legacy config bundle: provide legacy/ configs and a make conf9 target for easier onboarding of EOL PostgreSQL versions
Rework health probing architecture: use cached health snapshots with periodic probes for more consistent role-based health endpoints and smoother reload behavior
Improve release engineering baseline: run go test and go vet in release workflows and bump build toolchain to Go 1.26.0
Bug Fixes:
Fix multiple config parsing edge cases: reject malformed metrics entries, return explicit errors when config dirs fail to load valid YAML, and harden runtime fallbacks
Fix CLI bool flag parsing to correctly handle --flag=false style arguments
Fix /explain output/rendering behavior by adjusting content type handling and using safer template rendering
Change min_version from 9.6 to 10, explicit ::int type casting
pg_size: Fix log directory size detection, use logging_collector check instead of path pattern matching
pg_table: Performance optimization, replace LATERAL subqueries with JOIN for better query performance; fix tuples and frozenxid metric type from COUNTER to GAUGE; increase timeout from 1s to 2s
pg_vacuuming: Add PG17 collector branch with new metrics indexes_total, indexes_processed, dead_tuple_bytes for index vacuum progress tracking
pg_query: Increase timeout from 1s to 2s for high-load scenarios
Remove the monitor schema requirement for pg_query collectors (you have to ensure it with search_path or just
install pg_stat_statements in the default public schema)
Fix pgbouncer version parsing message level from info to debug