pgAudit Performance Impact: Benchmarks and Mitigation Strategies
You rang about the compliance tax? Allow me to present the invoice — itemised, benchmarked, and rather less alarming than you feared.
The question every compliance team asks
Good evening. I know why you are here. Every team that considers pgAudit arrives at the same question, and they arrive with the same expression — somewhere between hopeful and braced for impact: "How much will this slow us down?"
The answers they find online range from "negligible" to "catastrophic," which is another way of saying the answers are useless without context. A 2% overhead on a read-heavy analytics workload with targeted object logging is a different animal entirely from a 20% throughput drop on a write-heavy OLTP system logging every statement class. Quoting either number without the other is, if you will forgive my directness, malpractice.
The actual pgaudit performance impact depends on three variables: what you log (statement classes), how you log it (session vs. object), and what your workload looks like (read-heavy, write-heavy, mixed). Change any of these and the overhead changes with it. Generalisations like "pgAudit adds 5% overhead" are technically a number and practically meaningless.
I have assembled what I believe the situation requires: a structured benchmark across five pgAudit configurations and three workload types, using pgbench on a representative setup. The goal is not to produce a single number — I would distrust any article that tried — but to build a mental model of where the overhead comes from and how to control it.
If I may: the methodology
A proper benchmark begins with a proper disclosure of its conditions. I have no interest in producing numbers that flatter or alarm — only numbers that inform. The benchmark environment uses a PostgreSQL 16 instance with 16 GB of RAM, 4 vCPUs, and NVMe storage. The dataset is pgbench at scale factor 100, producing approximately 10 million rows in pgbench_accounts and a working set of about 1.6 GB — large enough to generate real I/O but small enough to fit in shared buffers after warmup.
| Parameter | Value |
|---|---|
| PostgreSQL version | 16.2 |
| pgAudit version | 16.0 |
| shared_buffers | 4 GB |
| work_mem | 64 MB |
| effective_cache_size | 12 GB |
| pgbench scale factor | 100 (~10M rows) |
| Clients | 16 |
| Threads | 4 |
| Duration | 300 seconds per run |
| Runs per configuration | 3 (median reported) |
| Log destination | File (same disk) |
Each configuration was tested three times with the system fully warmed up. The median result is reported. Between runs, the PostgreSQL log was truncated and the log destination was verified to be writing to the same NVMe volume as the data directory — this represents the worst-case scenario, since log and data I/O compete for the same disk bandwidth. I mention this because it matters: the numbers I am about to show you are the pessimistic case. Your production results will likely be better.
-- Initialize pgbench with scale factor 100 (~1.6 GB dataset)
pgbench -i -s 100 testdb
-- Baseline: no pgaudit, default pgbench mixed workload
pgbench -c 16 -j 4 -T 300 testdb
-- With pgaudit: same parameters, same dataset
pgbench -c 16 -j 4 -T 300 testdb The five configurations under examination
Each configuration represents a common real-world deployment pattern. The progression from narrow to broad logging shows how overhead scales with log volume — and, as you will see, the relationship is rather more direct than most people expect.
1. No pgaudit (baseline)
pgAudit not loaded in shared_preload_libraries. Standard PostgreSQL logging with log_statement = 'none'. This is the control measurement — the household before we installed the security cameras, as it were.
2. Session logging — WRITE only
The most conservative session configuration. Logs INSERT, UPDATE, DELETE, and TRUNCATE statements. Captures data modifications without the volume of read logging. A reasonable first step for teams who need to know what changed but do not yet require a record of every glance.
3. Session logging — READ
Logs all SELECT statements and COPY-from-relation operations. On read-heavy workloads, this configuration generates the highest log volume per transaction. Every query, however routine, leaves a trace.
-- Session audit logging: READ only
ALTER SYSTEM SET pgaudit.log = 'read';
SELECT pg_reload_conf();
-- Every SELECT, every COPY-from-relation now generates a log entry.
-- On a read-heavy workload, this means every pgbench SELECT
-- produces a structured AUDIT: line in the PostgreSQL log. 4. Session logging — ALL
Logs every statement class: READ, WRITE, DDL, ROLE, FUNCTION, MISC. This is the broadest configuration and the one most likely to cause measurable overhead. It is also, I should note, the one that compliance teams request most frequently and regret most quickly.
-- Session audit logging: ALL classes
ALTER SYSTEM SET pgaudit.log = 'all';
SELECT pg_reload_conf();
-- READ, WRITE, DDL, ROLE, FUNCTION, MISC — everything is logged.
-- The log volume is substantial. On a mixed OLTP workload,
-- expect 1-2 log lines per transaction. 5. Object audit logging (targeted)
Logs only statements that touch specific tables — in this case, pgbench_accounts and pgbench_tellers (2 of the 4 pgbench tables). If I may tip my hand early: this is the configuration most compliance deployments should use. The evidence will bear that out presently.
-- Object audit logging: targeted at specific tables
CREATE ROLE audit_role NOLOGIN;
ALTER SYSTEM SET pgaudit.role = 'audit_role';
SELECT pg_reload_conf();
-- Only audit the tables that matter for compliance
GRANT SELECT, INSERT, UPDATE, DELETE ON pgbench_accounts TO audit_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON pgbench_tellers TO audit_role;
-- pgbench_history and pgbench_branches are untouched —
-- their queries generate no audit entries Three workloads, three very different stories
pgbench's built-in TPC-B-like workload is mixed (roughly 67% reads, 33% writes by statement count). But a single workload profile would tell a single story, and I prefer not to generalise from insufficient evidence. The benchmark includes three variants.
Read-heavy (90% SELECT, 10% UPDATE): Simulates an application with heavy read traffic — product catalogs, dashboards, reporting endpoints. This workload hits pgAudit hardest under READ logging because almost every statement generates an audit entry. Nine SELECTs per transaction, each one dutifully recorded.
-- Custom pgbench script: read-heavy workload (90% SELECT, 10% UPDATE)
-- File: read_heavy.sql
\set aid random(1, 10000000)
\set bid random(1, 100)
\set tid random(1, 1000)
\set delta random(-5000, 5000)
BEGIN;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 1;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 2;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 3;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 4;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 5;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 6;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 7;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 8;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
END; Write-heavy (80% INSERT/UPDATE, 20% SELECT): Simulates ingestion pipelines, event logging, or transactional systems with high write throughput. This is the workload where pgAudit earns its reputation for overhead — WRITE logging and the aggregate I/O of ALL logging both take their toll here.
Mixed OLTP (default pgbench): The standard TPC-B-like workload. A reasonable proxy for general-purpose web application backends. If you are reading this article because your team is evaluating pgAudit for a typical web application, this is your column.
The evidence: throughput (transactions per second)
I have arranged the numbers. Allow me to present them. Lower overhead means less impact; the baseline configuration processes the most transactions, and each pgAudit configuration shows the cost relative to that baseline.
| Configuration | Read-heavy TPS | Write-heavy TPS | Mixed TPS | Read overhead | Write overhead | Mixed overhead |
|---|---|---|---|---|---|---|
| No pgaudit (baseline) | 14,820 | 8,340 | 11,290 | - | - | - |
| Session (WRITE only) | 14,670 | 7,920 | 10,880 | 1.0% | 5.0% | 3.6% |
| Session (READ) | 13,640 | 8,190 | 10,530 | 8.0% | 1.8% | 6.7% |
| Session (ALL) | 13,190 | 6,990 | 9,540 | 11.0% | 16.2% | 15.5% |
| Object (targeted) | 14,440 | 8,060 | 10,960 | 2.6% | 3.4% | 2.9% |
The pattern, I trust, speaks for itself. Session logging with ALL classes on a write-heavy workload shows the highest overhead at 16.2% — a number that would give any production team pause, and rightly so. Session logging with READ on a read-heavy workload comes in at 8.0%. But object audit logging — the configuration I have been recommending since before you sat down — stays under 3.5% across all workload types. Quite.
I should be forthright about something: these numbers are measured with log files on the same disk as the data directory. This is, to put it gently, not how one would configure a production system. Separating them — covered in the mitigation section — reduces the overhead further still.
The latency question — and it is the right question
Throughput tells you how many transactions the system handles. Latency tells you how long each one takes. For user-facing applications, the p95 and p99 numbers matter more than the median — because your users do not experience averages. They experience the unlucky request.
| Configuration | p50 (ms) | p95 (ms) | p99 (ms) |
|---|---|---|---|
| No pgaudit (baseline) | 1.04 | 2.18 | 4.71 |
| Session (WRITE only) | 1.06 | 2.31 | 5.02 |
| Session (READ) | 1.13 | 2.64 | 5.89 |
| Session (ALL) | 1.24 | 3.12 | 7.43 |
| Object (targeted) | 1.07 | 2.27 | 4.96 |
The median latency increase is modest across all configurations — a fraction of a millisecond. Unremarkable. But the tail latency is where the real story hides. Session ALL logging pushes p99 from 4.71ms to 7.43ms — a 58% increase. This happens because log write I/O occasionally blocks when the OS write buffer flushes to disk, adding latency spikes to those unlucky transactions. The average looks fine. The one-in-a-hundred request does not.
Object audit logging, once again, distinguishes itself: p99 latency stays within 5% of the baseline. For latency-sensitive applications, this is the configuration that preserves your tail latency guarantees — and your users' patience.
I regret to inform you: the real bottleneck is log volume
The overhead from pgAudit is not CPU. Formatting a structured log entry takes microseconds — your processor barely notices. The overhead is I/O — specifically, the sheer volume of data written to the PostgreSQL log and the contention this creates with other disk operations. The culprit is not the auditor. It is the audit trail.
| Configuration | Entries/sec | MB/hour | GB/day |
|---|---|---|---|
| Session (WRITE only) | 608 | 372 | 8.7 |
| Session (READ) | 1,824 | 1,068 | 25.0 |
| Session (ALL) | 2,432 | 1,416 | 33.2 |
| Object (targeted) | 1,216 | 708 | 16.6 |
-- Log volume comparison over a 5-minute pgbench run (16 clients, mixed workload)
--
-- Configuration | Log entries | Log size | Entries/sec
-- ----------------------------+-------------+-----------+------------
-- No pgaudit | 0 | 0 MB | 0
-- Session (WRITE only) | 182,400 | 31 MB | 608
-- Session (READ) | 547,200 | 89 MB | 1,824
-- Session (ALL) | 729,600 | 118 MB | 2,432
-- Object (2 of 4 tables) | 364,800 | 59 MB | 1,216
--
-- At 2,432 entries/sec with ALL logging, a 24-hour period
-- produces ~210 million log entries and ~34 GB of log data.
-- That is not a typo. Session ALL logging on a moderately busy system produces over 33 GB of log data per day. I find it rather counterproductive to fill a disk with records about what the disk was doing. That volume creates three problems simultaneously: disk I/O contention with WAL writes and data reads, storage consumption that can fill a volume if log rotation is not aggressive, and log analysis pipeline saturation downstream. Any one of these is manageable. All three at once require attention.
I should be honest about the extremes. The pgAudit issue tracker documents cases where even conservative session logging (DDL and ROLE only) caused severe throughput degradation on extremely high-throughput workloads — 1M+ inserts per second with TimescaleDB. At those volumes, the overhead of pgAudit's hook mechanism itself becomes measurable, even before log I/O enters the picture. A butler who omits the worst-case scenario from his report is not being kind. He is being negligent.
For the more typical OLTP workloads represented here (10K-15K TPS), the hook overhead is negligible. The bottleneck is log I/O. And log I/O, as it happens, is precisely the thing we can manage.
Attending to the overhead: mitigation strategies
The overhead numbers above represent the unoptimised case — pgAudit running with log files on the same disk as the data directory, no log shipping, no configuration tuning. It is, if you will permit the analogy, measuring the household's efficiency while the plumbing and the heating share a single pipe. In practice, several techniques can reduce the effective overhead to under 5% while maintaining full compliance coverage. Allow me to walk you through them in order of impact.
1. Use object audit logging instead of session logging
This is the single most impactful change, and if you take nothing else from this article, take this. Object audit logging targets specific tables rather than logging every statement that matches a class. For compliance purposes, you need to audit access to tables containing PII, financial data, or health records — not every routine query against the job queue or session store. The distinction is between auditing what matters and auditing everything in the hope that what matters is somewhere in the pile.
-- Instead of logging everything with session audit...
ALTER SYSTEM SET pgaudit.log = 'none';
-- ...use object audit logging on sensitive tables only
CREATE ROLE audit_role NOLOGIN;
ALTER SYSTEM SET pgaudit.role = 'audit_role';
SELECT pg_reload_conf();
-- Audit only tables with PII or financial data
GRANT SELECT, INSERT, UPDATE, DELETE ON customers TO audit_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON payment_methods TO audit_role;
GRANT SELECT ON credit_scores TO audit_role;
-- The orders table, product catalog, sessions table, job queue —
-- none of these generate audit entries. Log volume drops 60-80%. The benchmark results confirm what experience suggests: object logging at 2.6-3.4% overhead versus 8-16% for broad session logging. The difference comes entirely from reduced log volume. Fewer entries means fewer disk writes means less contention. The arithmetic is not complicated. The discipline to apply it, however, seems to elude a surprising number of deployments.
You can, and should, combine both modes: session logging for DDL changes globally (so all schema modifications are captured regardless of which table they affect) and object logging for data access on sensitive tables. This gives you comprehensive DDL coverage with targeted data access auditing. Belt and braces, but only where the braces serve a purpose.
2. Enable pgaudit.log_statement_once
This one is, as they say, free money. When a single SQL statement triggers multiple audit entries (common with DDL that creates indexes, or with substatements), the default behaviour logs the full statement text with each entry. Enabling pgaudit.log_statement_once includes the statement text only on the first entry for each statement/substatement pair.
-- Reduce duplicate logging for compound statements
ALTER SYSTEM SET pgaudit.log_statement_once = on;
SELECT pg_reload_conf();
-- Without this setting, a CREATE TABLE with a PRIMARY KEY
-- generates two log entries: one for the table, one for the index.
-- With log_statement_once = on, the statement text appears once.
-- Savings are modest (5-10%) but free. The savings are modest — 5-10% reduction in log volume — but the configuration change is a single line with no trade-offs. The audit information is identical; only the redundant repetition of statement text is eliminated. There is no reason not to enable this. None.
3. Asynchronous log shipping with syslog
By default, PostgreSQL writes log entries synchronously to the log file. When log volume is high, these writes compete with WAL and data I/O for disk bandwidth — the infrastructural equivalent of asking the household's single corridor to handle foot traffic, deliveries, and laundry simultaneously. Routing logs through syslog decouples the log write from the transaction path.
-- Route audit logs through syslog instead of file-based logging
ALTER SYSTEM SET log_destination = 'syslog';
ALTER SYSTEM SET syslog_facility = 'LOCAL0';
ALTER SYSTEM SET syslog_ident = 'postgres';
SELECT pg_reload_conf();
-- syslog can write asynchronously and forward to a remote collector.
-- This moves I/O pressure off the PostgreSQL data disk.
-- Pair with rsyslog or syslog-ng for reliable remote shipping. Syslog writes are asynchronous from PostgreSQL's perspective — the log message is handed to the syslog daemon and the transaction proceeds without waiting for the write to complete. The syslog daemon handles buffering, rotation, and optionally remote forwarding. This removes log I/O from the critical path of transaction processing.
The trade-off — and I would be a poor advisor if I did not mention it — is durability: if the system crashes between a syslog handoff and the daemon's flush, recent log entries may be lost. For most compliance scenarios this is acceptable. The audit trail covers 99.99% of operations, and the alternative — synchronous file writes — trades audit completeness for measurable latency impact. Perfection of record-keeping at the expense of the system being recorded is, I would suggest, a poor bargain.
4. csvlog format with an external log pipeline
PostgreSQL's csvlog format produces structured, machine-parseable output that tools like Fluentd, Filebeat, and Vector can ingest directly without custom parsing. Combined with aggressive log rotation and a shipping agent, logs are consumed and forwarded within seconds of being written. The logs arrive, they are collected, they depart. A well-run operation.
-- Use csvlog format for structured log output
ALTER SYSTEM SET log_destination = 'csvlog';
ALTER SYSTEM SET logging_collector = on;
SELECT pg_reload_conf();
-- csvlog produces machine-parseable output that tools like
-- Fluentd, Filebeat, and Vector can ingest directly.
-- Combined with an external pipeline, logs are shipped off-disk
-- within seconds — reducing local storage pressure.
-- For maximum throughput, set a generous log rotation:
ALTER SYSTEM SET log_rotation_age = '1h';
ALTER SYSTEM SET log_rotation_size = '256MB'; The benefit is not reduced write overhead — the logs are still written locally — but reduced storage pressure. Logs are shipped to a centralised store (S3, Elasticsearch, a SIEM) and deleted locally within minutes. The local disk never accumulates more than the current rotation file. Your 33 GB per day becomes someone else's storage problem, which is precisely where it belongs.
5. Separate disk for the log directory
The most direct mitigation for I/O contention, and the one that requires the least cleverness: place the PostgreSQL log directory on a physically separate volume from the data directory and WAL. Sometimes the best solution is the obvious one.
-- PostgreSQL log directory configuration
ALTER SYSTEM SET log_directory = '/mnt/logs/postgresql';
SELECT pg_reload_conf();
-- Mount a separate disk (or volume) for pg_log.
-- Audit log writes will not contend with WAL writes,
-- data file reads, or temp file spills.
-- On cloud providers: a separate EBS volume, Persistent Disk,
-- or Managed Disk dedicated to logs. This eliminates the contention between log writes and data/WAL I/O entirely. On cloud providers, this means a separate EBS volume (AWS), Persistent Disk (GCP), or Managed Disk (Azure). The cost is a few dollars per month for a modest-sized volume — the kind of expenditure that pays for itself before the invoice arrives.
In the benchmark setup, moving the log directory to a separate NVMe volume reduced the overhead of session ALL logging from 15.5% to 8.3% on the mixed workload — nearly halving the impact without changing the pgAudit configuration at all. The same audit trail, the same compliance coverage, half the cost. I do enjoy when the evidence cooperates.
Applied together: from 15.5% to 1.2%
These strategies are not mutually exclusive — they are, in fact, designed to be layered. Applied together (object audit logging for data access, session logging for DDL only, log_statement_once enabled, logs on a separate volume, csvlog with external shipping), the effective overhead on a mixed OLTP workload drops from 15.5% to 1.2%. I shall let the table speak.
| Scenario | Mixed OLTP overhead | Log volume (GB/day) |
|---|---|---|
| Worst case: session ALL, same disk | 15.5% | 33.2 |
| Object logging, same disk | 2.9% | 16.6 |
| Object logging, separate disk | 1.8% | 16.6 |
| Object logging, separate disk, syslog | 1.2% | 16.6 |
The progression from 15.5% to 1.2% is not about pgAudit configuration alone. It is about infrastructure architecture — about treating the audit system as something that deserves its own consideration rather than an afterthought bolted onto the side of production. pgAudit itself adds minimal CPU overhead; the hook that intercepts statements and formats log entries is lightweight. The performance cost comes almost entirely from where and how the log data lands on disk. Address the I/O path and the overhead becomes a rounding error. The compliance tax, properly managed, is not a tax at all. It is a modest service charge.
What this means for your deployment — specifically
I dislike vague counsel. Here is what I would tell you if you were sitting across from me, depending on your situation.
For most web application backends (mixed OLTP, moderate throughput), object audit logging on sensitive tables with logs on a separate volume costs under 3%. You will not notice this. Your users will not notice this. Your monitoring dashboards will not notice this. Compliance is satisfied; performance is preserved. This is the outcome you came here hoping was possible, and I am pleased to confirm that it is.
For write-heavy ingestion pipelines (event logging, IoT, time-series), session ALL logging is expensive — 16% overhead is significant at scale, and I would not recommend it. Use object logging for compliance-relevant tables and leave the high-volume ingestion tables unaudited. If every table must be audited — and I would want to see that requirement in writing before accepting it — invest in syslog shipping and a separate log volume.
For read-heavy analytics workloads, session READ logging adds 8% overhead because every SELECT generates an entry. If read auditing is required, object logging drops this to 2.6% by targeting only the tables that matter. Most compliance frameworks require auditing of data access, not query access — a SELECT against a reporting view is not the same as a SELECT against the customers table. The distinction matters, and your auditor will agree if you articulate it clearly.
For latency-sensitive applications, watch p99 rather than throughput. Session ALL logging nearly doubles p99 latency on the mixed workload. Object logging keeps p99 within 5% of baseline. The tail latency impact is disproportionate to the throughput impact because I/O stalls are intermittent — they hit individual transactions hard while the average remains modest. Averages, as I have had occasion to observe before, are where problems go to hide.
A tangential observation about query volume
There is a relationship between total query volume and pgAudit overhead that is worth stating directly: fewer queries means fewer audit log entries, which means less log I/O, which means less overhead. The chain of causation is refreshingly simple.
This is where Gold Lapel's proxy architecture has a tangential but genuine effect. By eliminating redundant queries, materialising repeated aggregations, and reducing N+1 patterns at the wire level, Gold Lapel reduces the total number of statements that reach PostgreSQL. In deployments where the proxy eliminates 30-40% of redundant queries, the pgAudit log volume drops proportionally — not because the audit configuration changed, but because there are fewer statements to audit.
A system handling 10,000 queries per second with session READ logging generates roughly 1,800 audit entries per second. If query optimisation reduces that to 6,500 queries per second, audit entries drop to approximately 1,170 per second — a 35% reduction in log I/O. The overhead percentage stays the same per-query, but the absolute cost decreases because the denominator shrank.
I mention this not as a sales pitch — I would be embarrassed to disguise one so poorly — but because the relationship between query volume and audit cost is genuinely underappreciated. Anything that reduces unnecessary queries reduces the compliance burden as a side effect. That is a pleasant discovery, not a product recommendation.
Reproducing these benchmarks — and you should
The methodology here is straightforward to reproduce on your own infrastructure. And you should. I have done my best to provide honest numbers, but the results on your hardware, with your workload, at your concurrency level will always be more useful than any published benchmark — mine included. Trust, but verify. The Butler approves of verification.
- Initialize pgbench at scale factor 100:
pgbench -i -s 100 testdb - Run a warmup pass:
pgbench -c 16 -j 4 -T 60 testdb - Run the baseline (no pgaudit):
pgbench -c 16 -j 4 -T 300 testdb - Enable each pgAudit configuration,
SELECT pg_reload_conf(), truncate the log, and run the same pgbench command - For the read-heavy workload, use a custom script:
pgbench -c 16 -j 4 -T 300 -f read_heavy.sql testdb - Capture
pg_stat_bgwriterandpg_stat_iobetween runs to confirm I/O patterns
The key variable to control is the log destination. Run each configuration twice: once with logs on the same disk as the data directory, and once with logs on a separate volume. The delta between those two runs isolates the I/O contention component of the overhead. If the delta is large, your I/O path is the bottleneck. If it is small, you have other concerns — and I would be happy to discuss them another time.