Neither of these parameters means what its name says. max_parallel_workers_per_gather is the most workers the planner is allowed to request for one plan node, and the planner usually requests fewer. max_parallel_workers is documented as a limit for the whole cluster, and it is enforced by comparing a cluster-wide count against a number that every session gets to choose for itself.
max_parallel_workers_per_gather defaults to 2 and max_parallel_workers to 8. Both run from 0 to 1024, and both are context user, so any role can change either one with a plain SET. max_parallel_workers_per_gather arrived in 9.6 along with parallel query, defaulting to 0. PostgreSQL 10 turned parallel query on by raising that default to 2, and added max_parallel_workers in the same release so that parallel query could be kept from occupying every background worker slot on the server.
I went through how the worker pools nest in Workers of the World, Unite!. Briefly: max_worker_processes is the entire pool of background workers, shared with logical replication (max_logical_replication_workers) and with whatever your extensions register, and it is the only one of the three that needs a restart. max_parallel_workers is how much of that pool parallel operations may hold at one time. That means parallel query, and also parallel index builds and parallel VACUUM, whose per-command request is max_parallel_maintenance_workers. (In 19, beta 3 as I write this, autovacuum joins them: autovacuum_max_parallel_workers, default 0, draws on the same budget.) max_parallel_workers_per_gather is what a single Gather or Gather Merge node may ask for.
What the planner asks for
Nothing outside the optimizer reads max_parallel_workers_per_gather. For a scan, the planner chooses a worker count from the amount it expects to read: one worker at min_parallel_table_scan_size, which is 8MB by default, and one more every time that figure triples. 24MB gets two workers, 72MB three, 216MB four, 648MB five, 1.9GB six, 5.7GB seven, and the eighth shows up at 17GB. The tenth needs 154GB. The largest table PostgreSQL can store, 32TB, gets fourteen. The comment above this loop says it “probably needs to be a good deal more sophisticated,” and has said so since 9.6.
max_parallel_workers_per_gather is then applied as a cap on that number. Here is a 270MB table on 18.6:
1 => SET max_parallel_workers_per_gather = 64;
2 => EXPLAIN (COSTS OFF) SELECT count(*) FROM t WHERE k = 7;
3 Finalize Aggregate
4 -> Gather
5 Workers Planned: 4
6 ...
Sixty-four was permission. Four is what 270MB earns. This is why raising the parameter to 16 on a database whose biggest table is 10GB changes less than people expect: nothing in it qualifies for more than seven. Two things get around the size rule. The parallel_workers storage parameter (ALTER TABLE t SET (parallel_workers = 16)) replaces the computed figure for that table. And with enable_parallel_append on, a Parallel Append asks for at least log2 of the partition count plus one, so 64 partitions ask for seven workers however small each one is. Both are still capped by max_parallel_workers_per_gather.
The name says “per gather,” and it means it. A plan with two Gather nodes can hold twice the limit; I watched a merge join between two Gather Merge subplans run four workers at once with the parameter at 2. The leader also executes the parallel part of the plan by default (parallel_leader_participation), so 2 means three processes, and each of them gets its own work_mem allowance.
A cluster-wide limit, set per session
max_parallel_workers is consulted in exactly one line of executable code in the PostgreSQL 18 tree: the check in RegisterDynamicBackgroundWorker() that refuses a new parallel worker when the number already running has reached the limit. The planner never looks at it. So setting it to 0 does not turn parallel query off. It gets you parallel plans that nobody shows up to help with:
1 => SET max_parallel_workers = 0;
2 => EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, BUFFERS OFF)
3 SELECT count(*) FROM t WHERE k = 7;
4 Finalize Aggregate (actual rows=1.00 loops=1)
5 -> Gather (actual rows=1.00 loops=1)
6 Workers Planned: 2
7 Workers Launched: 0
8 ...
The leader runs the whole thing alone, using a plan that was costed on the assumption of help. If you want parallel query off, the setting is max_parallel_workers_per_gather = 0.
A refused worker raises no error and writes nothing to the log. The query runs with whatever it got, and the only evidence is the gap between Workers Planned and Workers Launched in EXPLAIN (ANALYZE), which means you find it one query at a time, after someone complains. PostgreSQL 18 fixes that: pg_stat_database and pg_stat_statements both gained parallel_workers_to_launch and parallel_workers_launched, and a growing difference between the two is a pool that is too small. On 17 and earlier, count the backend_type = 'parallel worker' rows in pg_stat_activity (grouped by leader_pid, if you want to know whose they are) and compare the total with the limit.
Now the odd part. The running count in that check is cluster-wide, held in shared memory. The limit it is compared against is the calling session’s own value of a user-context parameter. With max_parallel_workers = 2 in postgresql.conf, here is a role with no privileges beyond SELECT on one table:
1 intern=> SHOW max_parallel_workers;
2 2
3 intern=> SET max_parallel_workers = 1024;
4 intern=> SET max_parallel_workers_per_gather = 64;
5 intern=> SET min_parallel_table_scan_size = 0;
6 intern=> EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, BUFFERS OFF)
7 SELECT count(*) FROM t WHERE k = 7;
8 Finalize Aggregate (actual rows=1.00 loops=1)
9 -> Gather (actual rows=8.00 loops=1)
10 Workers Planned: 10
11 Workers Launched: 7
12 ...
Seven, because max_worker_processes was still at its default of 8 and the logical replication launcher was sitting in one slot. While a longer version of that query ran, another session’s parallel plan launched zero workers, and a subscription created on the same server got no apply worker. CREATE SUBSCRIPTION succeeded; the only sign of trouble was the launcher writing this to the log every five seconds:
1 WARNING: out of background worker slots
2 HINT: You might need to increase "max_worker_processes".
Replication started by itself once the query was cancelled. A hostile intern is the unlikely version of this. The likely one is someone pasting SET max_parallel_workers = 64 from a blog post into the nightly reporting job. Lowering the value in a session is just as odd: the session gets no private budget of that size, only a rule that says “launch nothing for me unless fewer than this many parallel workers are running anywhere.”
The defaults don’t help. max_parallel_workers and max_worker_processes are both 8, which reserves nothing, and the launcher’s slot makes the ceiling on a stock install seven (six once one subscription is running). Setting max_parallel_workers higher than max_worker_processes is accepted at startup without a word.
So tune these in the opposite of alphabetical order. max_worker_processes comes first, because it needs a restart and because it is the only one of the three that is a hard limit: make it max_parallel_workers plus max_logical_replication_workers plus whatever your extensions register, plus four to eight spare. My numbers for max_parallel_workers are the same as in 2023: two to three times the core count, nearer 1.5 times on machines with 32 or more cores. That is generous on purpose, since the price of an exhausted pool is a parallel plan executed by one process. Leave max_parallel_workers_per_gather at 2 globally on anything that serves OLTP traffic, and raise it where the big scans are, with ALTER ROLE reporting SET max_parallel_workers_per_gather = 6. Going past 8 is pointless unless you are scanning tens of gigabytes at a time; for the one enormous table, set parallel_workers on that table. Then look through pg_db_role_setting, and through your application code and cron jobs, for anything that sets max_parallel_workers. Whatever you find there is your cluster-wide limit.