client_min_messages controls how much the server says to you: the messages that come back over the wire to your session. It is constantly confused with the parameter that controls what the server writes to its own log. Those are different concerns with different parameters, and the confusion is where most of the trouble starts.

The default is NOTICE, the context is user (so any role can set it per session, per role, or per database, with no restart involved), and the levels run in increasing severity: DEBUG5 through DEBUG1, then LOG, NOTICE, WARNING, ERROR. Set the parameter to a level and you get that level plus everything more severe; the higher you set it, the quieter your session gets. ERROR is as high as it goes. You cannot set it to FATAL or PANIC, because by the time those fire the server is no longer taking your formatting preferences into account.

Two things about that ladder will eventually bite you.

The first is LOG. In client_min_messages, LOG sits below NOTICE, down among the chatty low-priority levels. In , the parameter it’s perpetually mistaken for, LOG sits above WARNING, near the top. Same word, opposite rank, depending on which parameter you put it in. The docs note the discrepancy and decline to explain it; the practical reading is that a message worth writing to a server log is not necessarily a message worth interrupting a client with, and PostgreSQL ranks LOG accordingly in each context.

The second is INFO. It isn’t on the ladder at all, and it is always sent to the client no matter what you set. This is why raising client_min_messages to ERROR doesn’t make VACUUM VERBOSE shut up: its output is INFO, and INFO ignores you. If you have ever cranked this all the way up to silence a noisy session and watched verbose output keep arriving anyway, that’s the reason.

What people actually want this parameter for is silencing the NOTICE spam that schema tooling generates: the implicit-sequence notices from SERIAL columns, the “relation already exists, skipping” from CREATE TABLE IF NOT EXISTS, the “table does not exist, skipping” from DROP ... IF EXISTS. A migration that runs clean except for forty lines of those is a migration whose real warnings are buried in the noise. Put SET client_min_messages = WARNING; at the top of the script and the chatter disappears while genuine warnings still come through. That’s the correct move, and it’s a per-session SET, not a global change.

What you should not do is set it to ERROR server-wide because the notices annoy you. WARNING is where PostgreSQL tells you it’s quietly doing something you might not want: truncating a value, skipping a constraint check, falling back to a slower path. Suppressing that entire class globally to win back a few lines of output trades a real signal for cosmetics.

So leave the global default alone. This is a per-session tool: WARNING in your migration scripts, DEBUG1 when you’re actually debugging and want the server narrating, and NOTICE everywhere else by simply not touching it. The mistake here is almost never picking the wrong level. It’s setting it server-wide at all.