PostgreSQL: Why Turn On logging_collector Before Your Server Breaks in Production
Christophe Pettus explains the plumbing behind log_destination and logging_collector, and the trade-off that decides whether your database loses logs or hangs waiting on disk.

Logging isn't usually treated as an architecture concern, but in production it decides whether an incident is diagnosable or turns into a mystery. Christophe Pettus, in the "All Your GUCs in a Row" series published on Planet PostgreSQL, devotes an entire post to two parameters that almost every DBA sets on autopilot: log_destination and logging_collector. The central thesis is blunt and uncomfortable: you're going to want the collector turned on, and you'll probably find that out on a production server that's already running, when it's already too late.
The basic mechanism: everything is stderr
The foundation of PostgreSQL's logging system is simpler than the list of two dozen log_ parameters suggests. Unless syslog is requested, every message PostgreSQL emits is a write to the process's standard error*. The remaining parameters only refine what gets written, in what format, and who collects it on the other end.
log_destination is a comma-separated list, with a default of stderr and sighup context (reloadable without a restart). The options are stderr, csvlog (since 8.3), jsonlog (since 15), syslog, and, on Windows, eventlog.
logging_collector is a boolean, default off, postmaster context. Here is, according to Pettus, the single most consequential fact in the whole post: turning it on requires a restart. Not a reload, not a SET. It's stopping and starting the server.
What the collector actually does
With the collector off, stderr points to wherever the process that started the postmaster pointed it: a file via pg_ctl -l, the journal under systemd, the container's stdout under Docker. Every backend inherits that descriptor and writes each message with a single write(). It works, up to a point. There's no rotation, and on some platforms concurrent writers to the same file can interleave lines.
With the collector on, the postmaster creates a pipe, points its own stderr to the write end, and spawns a child process (postgres: logger in ps) that owns the read end and turns the stream into files. Every subsequent child already inherits stderr pointed at the pipe.
The implementation detail that matters: backends don't throw raw text into the pipe. Each message goes through a chunking protocol, with a header containing PID, length, destination format, and a last-chunk flag, in blocks never larger than PIPE_BUF (4096 bytes on Linux), so that every write is atomic. That's why the collector can reassemble a long message from one backend even when chunks from another backend arrive in between. That's the concrete reason auto_explain's multiline output comes out whole, not interleaved with its neighbor's.
The silent advantage over syslog
Anything that reaches the pipe without the header gets written directly to the log file as plain text. That's where a shared library's complaint shouted to stderr ends up, along with the output of a shell spawned by archive_command or by COPY ... TO PROGRAM, with no timestamp and no prefix, because it didn't come from PostgreSQL's own logging code.
As Pettus sums it up, this is the collector's quiet advantage over syslog: "the dynamic linker doesn't know how to call syslog()". In other words, low-level noise that would never pass through syslog still gets captured.
Two practical consequences. First: the plain-text .log file always exists with the collector on, regardless of what log_destination says, because the postmaster opens it before forking the logger (to prove log_directory is writable) and pass-through text needs somewhere to go. Set log_destination = 'csvlog' and you'll get a .csv with the messages and a .log with the line announcing that logging has moved to csvlog, plus whatever any shell decided to say.
Second: the logger only comes up after config files are read, the data directory is checked, and shared memory is allocated. Failures at any of those stages are reported to the original stderr, not to log_directory. If the server doesn't come up, look at the pg_ctl -l file or the journal before looking in log/.
The logger is also the only child the postmaster doesn't kill during a crash cycle. When a backend dies by signal and everything else is terminated and restarted, the logger keeps its PID and files, which is why was terminated by signal 9 shows up reliably in the log. Rounding out the picture are current_logfiles in the data directory and the pg_current_logfile() function (both since 10), which point to the file in use per format, and both are absent when the collector is off.
The trade-off that actually decides the configuration
Here's the part that separates whoever just flips the parameter from whoever understands what they signed up for. The pipe has fixed capacity, 64 kB by default on Linux. The collector was designed to never lose a message. When it falls behind, the pipe fills up, and the next process that tries to log blocks on write() until the collector drains it. Every process.
A log_directory on slow storage, or a burst of log_statement = 'all' on a busy server, can hang the entire instance behind its own log file. And the worst part: it presents as everything slowing down at once, for no reason pg_stat_activity can show, because the backends are stuck in a kernel write, not in a wait event.
syslog resolves the same situation by throwing messages away. Neither choice is wrong, but you need to know which one you made. Summarizing the behavior under stress:
- Slow disk: the collector hangs the instance; syslog never hangs.
- Full disk: the collector's write fails, it logs a note to the original stderr and drops the message, without blocking backends; syslog also drops it.
- High load (log burst): the collector blocks so nothing gets lost; syslog drops lines.
syslog also has two limiting properties: it breaks long messages at 1024 bytes by default and drops them when it can't keep up.
Where you probably stand today
The defaults are the least interesting configuration, because almost nobody runs them. Debian and Ubuntu leave the collector off and bring up each cluster with pg_ctl -l /var/log/postgresql/postgresql-18-main.log, rotating weekly via logrotate with copytruncate. This is exactly the configuration the documentation classifies as suitable only for low volume, running on a huge fraction of the world's PostgreSQL servers.
It works, but for a fragile reason: each message is a single write() to a file opened in append mode, and Linux, in practice, doesn't interleave these writes. What bites is copytruncate: lines written between the copy and the truncate are lost, and on a busy server that window isn't empty.
PGDG's RPMs turn the collector on and rotate daily inside log/ in the data directory. The official Docker image leaves it off so that docker logs works; turn it on inside the container and the logs migrate to the data volume, out of whatever your platform collects. Managed services turn it on and don't ask.
The practical recommendation
Pettus closes with a straightforward recipe: collector on, restart, log_destination = 'stderr', adding jsonlog (or csvlog before 15) if something other than a human reads the logs, keeping stderr in the list so pass-through text has somewhere to live. The preference for jsonlog over csvlog has a technical reason: CSV lines contain embedded line breaks in the query and context fields, and every parser "that handles multiline values" handles them in a slightly different way.
There's also a silent trap that's only incompletely documented: with the collector off, setting jsonlog or csvlog simply does nothing, with no warning. In Pettus's test, log_destination = 'jsonlog' with the collector off produced plain text on stderr, no warning, and pg_current_logfile() returned null. If you set this up on a Debian box and the JSON vanished, that's where it went.
Only use syslog if everything else on the host already goes through syslog and you've made peace with it dropping lines. And if you're on Debian and it's working, understand that it works because of a kernel property and a weekly bet, and turn the collector on before that bet stops paying off.
Translated from the Brazilian Portuguese original · Read the original
Web tool inspects PostgreSQL pg_dump without restoring to a server
PostgreSQL Dump Viewer replays the backup file inside the browser to check tables, foreign keys, and run read-only SQL before any real restore.


