DB Internals AtlasPostgreSQL 18.6 Home About

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

  1. The planner chooses a Gather (or Gather Merge) node with N planned workers, based on table size (min_parallel_table_scan_size), costs (parallel_setup_cost, parallel_tuple_cost) and max_parallel_workers_per_gather.
  2. 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_workers and max_worker_processes allow.
  3. 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.
  4. 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 Launched lower than planned: the pool is exhausted. Raise max_worker_processes and max_parallel_workers together (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 parallel CREATE INDEX and VACUUM index processing, again from the same pool.
  • Each worker is a full process with its own work_mem per operation: a parallel hash join may use (workers + 1) × work_mem × hash_mem_multiplier.
  • DSM lives in /dev/shm by default (dynamic_shared_memory_type = posix); a tiny /dev/shm in 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

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 in shared_preload_libraries; beyond the slot count it logs too many background workers.
  • Dynamic: RegisterDynamicBackgroundWorker() takes BackgroundWorkerLock, finds a free slot (returns false if none, or if a BGWORKER_CLASS_PARALLEL request would exceed max_parallel_workers), and sends PMSIGNAL_BACKGROUND_WORKER_CHANGE; the postmaster's BackgroundWorkerStateChange() picks it up and forks BackgroundWorkerMain(). WaitForBackgroundWorkerStartup() and GetBackgroundWorkerPid() use the returned BackgroundWorkerHandle.
  • Parallel query: ExecGather() → ExecInitParallelPlan() (CreateParallelContext("postgres", "ParallelQueryMain", n), InitializeParallelDSM(), serialized plan and tuple queues) → LaunchParallelWorkers(), which registers workers with bgw_function_name = "ParallelWorkerMain" and records nworkers_launched (the "Workers Launched" line).
  • In the worker, ParallelWorkerMain() attaches the DSM, restores GUCs, snapshot, transaction and combo-CID state, then calls ParallelQueryMain(). Errors travel back via the error shm_mq and are re-thrown by ProcessParallelMessages() in the leader.

Extension API and tests

  • src/test/modules/worker_spi is the reference background worker (static and dynamic, with a database connection via BackgroundWorkerInitializeConnection()). 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 with CreateParallelContext() and its own entry point (as parallel CREATE INDEX does with _bt_parallel_build_main).
  • debug_parallel_query = on forces 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