Cookbook recipe

Parallel Query Workers Not Scaling (Gather Bottleneck)

Applies to PostgreSQL 13–17 Last reviewed May 2026 Grounded in source
Estimated investigation4 min

Scenario

Scenario A data engineering team configures max_parallel_workers_per_gather = 4 expecting a 4× speedup on a large aggregation. After the change, EXPLAIN ANALYZE on their query shows Workers Planned: 4 but Workers Launched: 1, and the query…

Investigation Path

Scenario

A data engineering team configures max_parallel_workers_per_gather = 4 expecting a 4× speedup on a large aggregation. After the change, EXPLAIN ANALYZE on their query shows Workers Planned: 4 but Workers Launched: 1, and the query runs no faster than before. The table is 200 MB, well above what they think is the threshold. The query calls a custom UDF that was not explicitly marked as parallel-safe.

How to Identify

Conditions:

  • EXPLAIN ANALYZE shows a Gather (or Gather Merge) node with Workers Planned: N but Workers Launched: M where M < N
  • Query performance does not improve despite increasing max_parallel_workers_per_gather
  • Table may be smaller than min_parallel_table_scan_size (default 8 MB) — parallel plan not triggered
  • A PARALLEL UNSAFE function anywhere in the query silently disables all parallelism
  • max_worker_processes or max_parallel_workers is exhausted by other queries

Analysis Steps

-- 1. Check EXPLAIN output for Workers Planned vs Workers Launched
EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT TEXT)
SELECT region, COUNT(*), SUM(amount)
FROM   large_fact_table
GROUP  BY region;

-- 2. Check all parallel configuration parameters
SELECT name, setting
FROM   pg_settings
WHERE  name IN (
    'max_parallel_workers_per_gather',
    'max_parallel_workers',
    'max_worker_processes',
    'min_parallel_table_scan_size',
    'min_parallel_index_scan_size',
    'parallel_setup_cost',
    'parallel_tuple_cost'
)
ORDER BY name;

-- 3. Check if any function in the query is PARALLEL UNSAFE
SELECT proname, proparallel,
       CASE proparallel WHEN 's' THEN 'SAFE'
                        WHEN 'r' THEN 'RESTRICTED'
                        WHEN 'u' THEN 'UNSAFE' END AS safety
FROM   pg_proc
WHERE  pronamespace = 'public'::regnamespace
  AND  proparallel != 's'
ORDER  BY proname;

-- 4. Check table size vs min_parallel_table_scan_size
SELECT relname,
       pg_size_pretty(pg_relation_size(oid)) AS table_size,
       pg_relation_size(oid)                 AS size_bytes
FROM   pg_class
WHERE  relname = 'large_fact_table';

-- 5. Check per-table parallel_workers storage parameter
SELECT relname, reloptions
FROM   pg_class
WHERE  relname = 'large_fact_table';

Pitfalls

  • A single PARALLEL UNSAFE function anywhere in the query (including in WHERE, SELECT, ORDER BY) disables parallelism for the entire query — no warning is issued
  • Each parallel worker gets its own work_mem allocation; with 4 workers, peak memory = 4× work_mem plus the leader process
  • Setting max_parallel_workers_per_gather higher than physical CPU cores causes OS context-switching that degrades performance
  • min_parallel_table_scan_size (default 8 MB) prevents parallel plans on small tables even if workers are available
  • Partitioned tables require enable_partitionwise_aggregate = on to run each partition in a separate worker effectively

Resolution Approach

When workers are planned but not launched, address causes in this order:

This is a Pro lesson

Get every Learning Pathway and cookbook recipe — grounded in PostgreSQL source code, with diagnostics, fixes, and prevention for each topic.

Continue this lesson to learn:

  • Mitigation Actions
  • All 36 Learning Pathway lessons
  • 170+ cookbook recipes
  • Source-grounded diagnostics & fixes

Secure checkout Cancel anytime Source-grounded

Career Impact

This scenario builds production judgment and operational confidence under pressure.

Open Career Dashboard →

Keep going

Related & next steps

Was this helpful?

← All cookbook recipes