Background processes
Parallel and background workers
Background workers are processes the postmaster starts on request, from a fixed pool of max_worker_processes slots. Parallel query takes its helpers from that pool, so when it is busy a query runs with fewer workers than planned.
In plain words
Besides the fixed helpers (checkpointer, WAL writer …) PostgreSQL can start background workers on demand: for parallel queries, logical replication, and extensions such as pg_cron or TimescaleDB. They all come from one pool of max_worker_processes slots (default 8), sized at server start.
A parallel query is split by the planner: the session's own backend becomes the leader and asks for, say, 2 parallel workers. They scan parts of the table and send rows to the leader through dynamic shared memory. Their number is capped per query (max_parallel_workers_per_gather, default 2) and in total (max_parallel_workers, default 8).
Workers are not reserved: if the pool is empty when the query starts, it runs with fewer, or with none. EXPLAIN ANALYZE then shows Workers Planned: 2 but Workers Launched: 0, and the query is slower than expected. Raise the number of sessions to see it happen.
For DBAs and operators
The background worker framework
A background worker is registered either at start-up by a library in shared_preload_libraries (static, e.g. pg_cron's scheduler, TimescaleDB's launcher, pg_prewarm's autoprewarm leader) or dynamically by a running process (parallel query, logical replication workers, autoprewarm's per-database workers). The postmaster forks it into a free slot of max_worker_processes. When no slot is free, a dynamic registration fails; parallel query copes with that silently.
On a default PostgreSQL 18 server the logical replication launcher already occupies one slot, so 7 remain. Logical replication apply/tablesync/parallel apply workers, autoprewarm, and extension workers take more. Raising max_worker_processes needs a restart and must be at least as high on a standby as on its primary.
Parallel query workers
- The planner chooses a
Gather(orGather Merge) node with N planned workers, based on table size (min_parallel_table_scan_size), costs (parallel_setup_cost,parallel_tuple_cost) andmax_parallel_workers_per_gather. - At execution the leader creates a DSM segment (plan, parameters, snapshot, tuple queues, shared scan state) and registers N dynamic workers. Each gets a slot only if both
max_parallel_workersandmax_worker_processesallow. - Workers attach to the DSM, restore the leader's snapshot and transaction state, and run their part of the plan. The leader also executes the plan unless
parallel_leader_participation = off. - At the end workers exit and their slots are free again. Parallel workers never outlive the query.
Finalize Aggregate (actual rows=1.00 loops=1)
-> Gather (actual rows=1.00 loops=1)
Workers Planned: 2
Workers Launched: 0
-> Partial Aggregate (actual rows=1.00 loops=1)
-> Parallel Seq Scan on t (actual rows=2000000.00 loops=1)That plan (run with max_parallel_workers = 0) was still "parallel", but the leader did all the work: loops=1 instead of 3.
How to see them
SELECT pid, leader_pid, backend_type, query
FROM pg_stat_activity
WHERE backend_type IN ('parallel worker', 'background worker')
OR backend_type LIKE 'logical replication%';ps shows postgres: parallel worker for PID 1913; extension workers show their own backend_type (e.g. pg_cron launcher). A leader waiting for workers shows wait event ExecuteGather (IPC) or MessageQueueReceive.
Support notes
- Sporadic slow queries during busy hours with
Workers Launchedlower than planned: the pool is exhausted. Raisemax_worker_processesandmax_parallel_workerstogether (restart needed for the first), and keep CPU count in mind. - Logical replication workers that do not start, with WARNING: out of background worker slots HINT: You might need to increase "max_worker_processes". (logged by the process that tried to start the worker: the launcher for apply workers, an apply worker for tablesync and parallel apply workers) mean the slots are used up; count them with the query above. Extensions report a failed dynamic start in their own way; static registrations beyond the pool are refused at start-up with LOG: too many background workers
max_parallel_maintenance_workers(default 2) caps parallelCREATE INDEXandVACUUMindex processing, again from the same pool.- Each worker is a full process with its own
work_memper operation: a parallel hash join may use (workers + 1) ×work_mem×hash_mem_multiplier. - DSM lives in
/dev/shmby default (dynamic_shared_memory_type = posix); a tiny/dev/shmin containers gives ERROR: could not resize shared memory segment "/PostgreSQL.1234" to 8388608 bytes: No space left on device - If a parallel worker crashes (signal), the postmaster performs a crash restart as for any backend; an error in a worker is re-raised in the leader and the query fails.
For developers: inside the source
Where it lives
bgworker.c/bgworker.h: the background worker framework.parallel.c: generic parallel contexts (DSM, worker launch, error propagation), described inREADME.parallel.execParallel.candnodeGather.c/nodeGatherMerge.c: the parallel executor;tqueue.c: tuple queues overshm_mq.
Key data structures
BackgroundWorker is what a caller fills in: bgw_name, bgw_type (shown as backend_type), bgw_flags (BGWORKER_SHMEM_ACCESS, BGWORKER_BACKEND_DATABASE_CONNECTION, BGWORKER_CLASS_PARALLEL), bgw_start_time, bgw_restart_time (or BGW_NEVER_RESTART), bgw_library_name + bgw_function_name (looked up by name, not by pointer, so it works with EXEC_BACKEND), bgw_main_arg, bgw_extra and bgw_notify_pid. The shared BackgroundWorkerArray has max_worker_processes BackgroundWorkerSlots plus the counters parallel_register_count/parallel_terminate_count, whose difference is checked against max_parallel_workers.
Call flow
- Static:
RegisterBackgroundWorker()from_PG_init()of a library inshared_preload_libraries; beyond the slot count it logstoo many background workers. - Dynamic:
RegisterDynamicBackgroundWorker()takesBackgroundWorkerLock, finds a free slot (returnsfalseif none, or if aBGWORKER_CLASS_PARALLELrequest would exceedmax_parallel_workers), and sendsPMSIGNAL_BACKGROUND_WORKER_CHANGE; the postmaster'sBackgroundWorkerStateChange()picks it up and forksBackgroundWorkerMain().WaitForBackgroundWorkerStartup()andGetBackgroundWorkerPid()use the returnedBackgroundWorkerHandle. - Parallel query:
ExecGather()→ExecInitParallelPlan()(CreateParallelContext("postgres", "ParallelQueryMain", n),InitializeParallelDSM(), serialized plan and tuple queues) →LaunchParallelWorkers(), which registers workers withbgw_function_name = "ParallelWorkerMain"and recordsnworkers_launched(the "Workers Launched" line). - In the worker,
ParallelWorkerMain()attaches the DSM, restores GUCs, snapshot, transaction and combo-CID state, then callsParallelQueryMain(). Errors travel back via the errorshm_mqand are re-thrown byProcessParallelMessages()in the leader.
Extension API and tests
src/test/modules/worker_spiis the reference background worker (static and dynamic, with a database connection viaBackgroundWorkerInitializeConnection()). Docs:doc/src/sgml/bgworker.sgml.- Workers that need shared memory pair this with
shmem_request_hook/shmem_startup_hook; an extension can run its own parallel code withCreateParallelContext()and its own entry point (as parallelCREATE INDEXdoes with_bt_parallel_build_main). debug_parallel_query = onforces a Gather on top of parallel-safe plans; the regression suite runs that way to test parallel safety.
Parameters that shape this
max_worker_processes max_parallel_workers max_parallel_workers_per_gather max_parallel_maintenance_workers parallel_leader_participation min_dynamic_shared_memory dynamic_shared_memory_type max_logical_replication_workers
Related topics
Parallel query Logical replication workers Postmaster Client backend Memory architecture Local (per-backend) memory Planner cost model Process model