max_parallel_workers
Caps total parallel query workers. Default 8. Setting to 0 disables parallelism — analytics queries silently run 3× slower without warning.
Tier 1 — Lab-Proven — Measured behavior and safety boundaries are available below.
What We Observed
box
(PG 17.10, Intel Core Ultra 7 268V, 8 vCPU), mpw_test 2M rows, GROUP BY grp + ORDER BY sum
workers 0 sequential
- exec time ms reps
- 507.092
- 524.378
- 532.019
- median ms
524.4
- plan
Seq Scan (no parallelism)
workers 2 parallel
- exec time ms reps
- 195.927
- 167.099
- 177.311
- median ms
177.3
- speedup
2.96×
formula curve
=== max_parallel_workers Formula Curve === Table: mpw_test (2M rows), query: GROUP BY grp + ORDER BY sum
workers plan_type exec_ms 0 Seq Scan+ 501.087 1 Gather+Parallel+ 237.624 2 Gather+Parallel+ 185.252 4 Gather+Parallel+ 189.166
note
Diminishing returns visible: workers=2 → 185ms, workers=4 → 189ms (similar). More workers help until CPU-bound.
Reproduced Danger
Sizing / Safe Range
Workload Guidance
Unlock Incident Pro
The free sections tell you what to do. Pro is the measured proof — the exact failure signature, safe-ceiling formula, and per-workload defaults, each captured on real PG 14–18 runs.
- Reproduced Danger
- Sizing / Safe Range
- Workload Guidance
max_parallel_workers is the SERVER-WIDE POOL of parallel worker processes — the maximum number that can be active at one time across ALL sessions combined, for both parallel queries and parallel maintenance (CREATE INDEX, VACUUM). It sits in the middle of a three-level hierarchy: max_parallel_workers_per_gather caps how many workers ONE query's Gather node may use; max_parallel_workers caps how many can be busy across the WHOLE cluster at once; and max_worker_processes (set at the postmaster, RESTART-only) is the hard ceiling of background-worker slots that the pool itself draws from. So the binding relationship is per_gather ≤ max_parallel_workers ≤ max_worker_processes. When a query (or an index build) wants workers, the planner PLANS some number, then at execution PostgreSQL only LAUNCHES as many as the pool has free — if the pool is exhausted it silently runs with fewer, or with zero (fully serial), and there is no error. Default is 8.
- name
max_parallel_workers
- setting default
8
- unit
(none — a count of concurrently active parallel workers, server-wide)
- vartype
integer
- category
Resource Usage / Worker Processes
- context
user
- min val
0
- max val
1024
- enumvals
(n/a — integer)
- boot val
8
- reset val
8
- pending restart
false
- short desc
Sets the maximum number of parallel workers that can be active at one time.
- ground truth note
Captured literally from pg_settings on PG 14-18 (-18) on 2026-06-19. setting=8, boot_val=8, reset_val=8 on EVERY version (default_changed=false, boot_fallback_changed=false). context=user (any session/role/database can set it, no restart, no superuser). vartype=integer, min_val=0, max_val=1024, unit empty. category='Resource Usage / Worker Processes', short_desc='Sets the maximum number of parallel workers that can be active at one time.' Default and bounds are identical across 14, 15, 16, 17, 18.
Rock-stable across versions: default=8, min=0, max=1024, context=user, vartype=integer on PG 14, 15, 16, 17 and 18 — no version cliff (default_changed=false, boot_fallback_changed=false). The pool semantics are also unchanged: it is consulted at worker-registration time and compared against a shared count of currently-active parallel workers, for both parallel query and parallel maintenance. The surrounding machinery has evolved (more parallel-capable plan nodes over the years, parallel VACUUM since PG 13, parallel utility commands), but the value, bounds, and meaning of max_parallel_workers itself are constant 14→18. Per-version detail in _guc_matrix_machine.
context=user, so it is fully flexible: set it cluster-wide in postgresql.conf + reload, per-database (ALTER DATABASE x SET max_parallel_workers=.), per-role, or per-session (SET max_parallel_workers = N) — NO restart, NO superuser. Because the pool limit is checked against the launching backend's OWN value, all backends that should share a budget must carry the same value (set it globally for a true server-wide cap). It takes effect immediately on the next query/maintenance that asks for workers. CRITICAL companion: max_parallel_workers can never exceed the EFFECTIVE number of background-worker slots, which is max_worker_processes — and THAT is RESTART-only. Raising max_parallel_workers above max_worker_processes is inert; to actually get more parallel workers you must raise max_worker_processes (restart) AND max_parallel_workers together. The lab set it per session via PGOPTIONS '-c max_parallel_workers=N' and read Workers Launched back from EXPLAIN ANALYZE. BOUNDARY PROOF (captured literal): an out-of-range / invalid value is rejected at set time -- ALTER SYSTEM SET max_parallel_workers = -1; -> ERROR: -1 is outside the valid range for parameter "max_parallel_workers" (0 .. 1024). It is context=user, so a plain SET max_parallel_workers = ... in a session succeeds (session-scoped, no restart); change it cluster-wide via postgresql.conf or ALTER SYSTEM + reload.
Managed providers (Azure Database for PostgreSQL Flexible Server, Amazon RDS / Aurora, Cloud SQL, Crunchy, Supabase, Neon, Heroku) expose max_parallel_workers as a server parameter; because it is context=user you can also SET it per session/database/role without a restart. The companion max_worker_processes is RESTART-only on every provider (it triggers a restart when changed), and on smaller managed tiers it is often modest — so on a managed instance the pool is frequently capped by max_worker_processes, exactly as the lab showed. Default is 8 everywhere. Tuning advice is identical to self-hosted: size max_parallel_workers near vCPU count, keep max_worker_processes ≥ the pool, leave headroom for the provider's own background workers (managed maintenance, logical replication, extensions), and never set the pool to 0 unless you intend to disable all parallel query and maintenance.
Common Pitfalls
- Setting max_parallel_workers=0 to 'limit' parallelism — it is a server-wide KILL SWITCH that disables every parallel query AND parallel maintenance, and the plan still shows a Gather, so it looks like parallelism is on.
- Raising max_parallel_workers above max_worker_processes and expecting more workers — it is inert; the real ceiling is max_worker_processes, which needs a RESTART.
- Forgetting that parallel CREATE INDEX and parallel VACUUM draw from this same pool — a low pool silently slows maintenance even with a high max_parallel_maintenance_workers.
- Assuming Workers Planned = Workers Launched — under pool pressure Launched can be far lower (down to 0 = serial) with no error; always check Workers Launched in EXPLAIN ANALYZE, not just the plan shape.
- Sizing the pool for one query while many run — concurrent parallel queries share the pool; peak_concurrency × workers_each above the pool means some queries silently run serial (unfair, first-come rationing).
- Oversubscribing: setting the pool well above core count so launched workers + leaders exceed CPUs, causing context-switch thrash and regressions (the lab's pool=8 single query was slower than pool=4).
Evidence and References
- PostgreSQL docs: Runtime Config — Resource Consumption → Asynchronous Behavior (max_parallel_workers, max_parallel_workers_per_gather, max_worker_processes, max_parallel_maintenance_workers).
- PostgreSQL docs: Parallel Query — how Gather launches workers, leader participation, and Workers Planned vs Workers Launched.
- PostgreSQL docs: Parallel Query → When Can Parallel Query Be Used (per_gather, pool, and worker-process limits).
- PostgreSQL docs: CREATE INDEX (parallel index build, limited by max_parallel_maintenance_workers and max_parallel_workers).
- PostgreSQL docs: ALTER TABLE . SET (parallel_workers) per-table reloption.
- Lab capture 2026-06-19: , 20M-row par table; EXPLAIN ANALYZE Workers Planned/Launched, 4-way concurrency, parallel CREATE INDEX timing; pg_settings ground truth PG 14-18 (-18).
Technical Reference
- Category
- Resource Usage / Worker Processes
- Type
- integer ((none; a count — the maximum number of parallel worker processes that can be ACTIVE at one time across the whole server, drawn from the max_worker_processes pool))
- Blast radius
- query
- Severity
- Moderate
- Context
- user
- Versions
- 14, 15, 16, 17, 18
- Reviewed
- 2026-06-19
Severity scale: 1 Minimal · 2 Low · 3 Moderate · 4 High · 5 Critical.