October 12, 2026

The Twelve-Year Documentation Bug That Forced PostgreSQL 18 to Invent num_os_semaphores

the-twelve-year-documentation-bug-that-forced-postgresql-18-to-invent-num_os_semaphores

the-twelve-year-documentation-bug-that-forced-postgresql-18-to-invent-num_os_semaphores

For over a decade, PostgreSQL developers faced an embarrassing, slow-motion bookkeeping failure. Hidden deep within the documentation for the world’s most advanced open-source relational database was a manual calculation meant to help administrators determine how many operating-system semaphores the server would require at startup.

The trouble? That formula was wrong. It had been wrong, on and off, for twelve years.

Whenever a new architectural component—such as the checkpointer, the background writer, the archiver, or the WAL summarizer—was introduced to PostgreSQL, a hardcoded constant in the documentation was supposed to be incremented to account for the new process slots. Almost invariably, human maintainers forgot.

To end this cycle of documentation drift, PostgreSQL 18 introduces a new internal, read-only preset parameter: num_os_semaphores. Rather than relying on human memory or error-prone manual math, the database engine now dynamically calculates and reports its own semaphore consumption at startup.

Yet, the introduction of this simple diagnostic tool has pulled back the curtain on a much larger, thornier engineering saga—one involving rigid Unix kernels, archaic semaphore allocations, and the bitter reality of operating systems that refused to give PostgreSQL enough room to breathe.


Main Facts

At its core, num_os_semaphores is a straightforward diagnostic parameter. On a default PostgreSQL 18 installation, its value evaluates to 174.

Every slot the database server reserves for a backend or background process requires a single semaphore. Processes sleep on these semaphores while waiting for lightweight locks. Rather than allocating them dynamically under load, PostgreSQL creates all necessary semaphores upfront during initialization.

The math behind the parameter is derived from a core formula:
$$textmax_connections + textautovacuum_worker_slots + textmax_worker_processes + textmax_wal_senders + 40$$

The constant 40 is composed of:

  • Two singleton processes (the autovacuum launcher and the slot sync worker)
  • Six auxiliary processes (checkpointer, background writer, WAL writer, and others)
  • Thirty-two dedicated slots for I/O workers

Crucially, those 32 I/O worker slots are reserved regardless of the chosen io_method or io_workers configuration. Even if an administrator sets io_method = sync, those slots—and their corresponding semaphores—are not returned to the system. Meanwhile, prepared transactions do not consume semaphores, meaning max_prepared_transactions is excluded from the calculation.

Because num_os_semaphores is computed at runtime, administrators can query it even when the database server is completely offline. By running:

$ postgres -D $PGDATA -C num_os_semaphores

The system will instantly return the precise number of semaphores required, bypassing the need for a running postmaster process. (If the server is already running, administrators must use the SQL command SHOW num_os_semaphores; instead, as the command-line utility will halt when it encounters the active postmaster.pid lock file).


Chronology of a Chronic Calculation Error

The history of the PostgreSQL semaphore documentation error is a textbook study in the hazards of hardcoded documentation in complex software projects.

  • PostgreSQL 9.2 (2012): The checkpointer process was introduced, altering the background process architecture. The manual documentation formula failed to account for it, beginning a twelve-year streak of mathematical inaccuracy.
  • PostgreSQL 9.6: A contributor finally noticed the discrepancy between the documentation formula and the actual source code, prompting a temporary correction.
  • PostgreSQL 14: The archiver process was reclassified as an auxiliary process, silently breaking the formula once again.
  • PostgreSQL 17: The introduction of the WAL summarizer threatened to break the formula for a third time.
  • May 2024: Developer Nathan Bossart audited the historical record of the parameter. Realizing that maintaining a manual formula was a losing battle, Bossart proposed abandoning published sums entirely. Instead, the server would programmatically compute and report its own requirements. This work laid the foundation for PostgreSQL 18.
  • August 2026: Recognizing the widening gap between modern database requirements and legacy operating system limits, the OpenBSD project raised its default semaphore kernel limits from 60 to 350, effectively resolving years of compatibility friction.

Supporting Data: Modern POSIX vs. Legacy System V

How operating systems handle semaphores dictates whether an administrator will ever need to worry about num_os_semaphores.

The Modern Approach: Linux and FreeBSD

For nearly two decades (since version 10), PostgreSQL installations running on Linux and FreeBSD have utilized unnamed POSIX semaphores. Under this architecture:

  • Each semaphore consumes a modest 128 bytes inside the main shared memory segment (roughly 22 KB total for all 174 semaphores on a default build).
  • There are no discrete kernel objects tracking them individually.
  • Standard diagnostic utilities like ipcs -s reveal nothing because no System V IPC facilities are being utilized.
  • Windows relies on its own native synchronization primitives, bypassing the issue entirely.

The Legacy Approach: Solaris, macOS, NetBSD, and OpenBSD

Platforms that do not use unnamed POSIX semaphores rely on System V semaphores, which are tightly rationed by the kernel.

All Your GUCs in a Row: num_os_semaphores

PostgreSQL 18 allocates these semaphores in sets of 16, appending a 17th semaphore to each set acting as a "magic number" so the server can identify its own allocations. (By comparison, PostgreSQL 17 utilized sets of 19 plus one).

Two critical kernel limits govern this behavior:

  1. SEMMNI: The maximum number of semaphore identifiers (sets) allowed system-wide. This must be at least $lceiltextnum_os_semaphores / 16rceil$.
  2. SEMMNS: The total number of semaphores allowed system-wide. This must be at least the number of sets multiplied by 17.

Under default PostgreSQL 18 configurations, these constraints demand at least 11 sets and 187 total semaphores. If a legacy Unix kernel falls short of these figures, the database engine refuses to start, throwing a fatal error:

FATAL:  could not create semaphores: No space left on device
DETAIL:  Failed system call was semget(1017319, 17, 03600).
HINT:  This error does *not* mean that you have run out of disk space. [...]

(For those wondering, ENOSPC is the standard POSIX error code returned by semget() when system limits are breached. The accompanying hint has clearly been triggered by enough confused database administrators over the years to warrant permanent inclusion).


Official Responses and Engineering Realities

The release of PostgreSQL 18 brought the confrontation between modern database memory demands and legacy operating system limits to a head, specifically concerning NetBSD and OpenBSD.

For years, the stock SEMMNS limit on NetBSD and OpenBSD installations sat stubbornly at 60.

When comparing versions, the engineering math becomes stark:

  • A default PostgreSQL 17.11 installation required 129 semaphores.
  • A default PostgreSQL 18.6 installation requires 174 semaphores—an increase of 45 units driven primarily by the introduction of 32 dedicated I/O worker slots and an autovacuum_worker_slots default of 16 (up from an effective cap of 3 workers under older parameters).

Version 17.11 could barely squeeze under the 60-semaphore ceiling; initdb would automatically dial back max_connections to 25 just to allow the server to boot up using precisely 60 semaphores.

Version 18, however, broke this workaround entirely. The absolute minimum configuration that initdb can generate for PostgreSQL 18 requires 102 semaphores—well past the 60-semaphore limit of unpatched BSD kernels.

Faced with this impasse, core PostgreSQL developers made a pragmatic choice. The commit that sealed the behavior was bluntly titled: "Give up on running with NetBSD/OpenBSD’s default semaphore settings." The official documentation was updated to label those stock operating system defaults as "unworkably small."

Fortunately, operating system maintainers took notice. OpenBSD updated its source tree defaults to 25 sets and 350 semaphores, paving the way for smooth deployments. Under these revised boundaries, a default PostgreSQL 18.6 cluster boots cleanly and scales smoothly up to max_connections = 246. Crossing 247 connections triggers the familiar ENOSPC fatal abort.


Implications for Database Administrators

For database administrators managing PostgreSQL on System V platforms (such as NetBSD, OpenBSD, or older Unix environments), the lesson is clear: proactive kernel tuning is mandatory before running initdb.

When operating system limits are tight but not entirely hopeless, initdb will quietly engage in a self-preservation routine. Rather than throwing a fatal error, it will test progressively smaller max_connections values—stepping down from 100 to 50, 40, 30, and 20—until it finds a threshold that fits within the kernel’s constrained semaphore budget. An administrator might miss this entirely, noticing only a brief, ambiguous line in the initialization logs:

selecting default "max_connections" ... 50

To avoid silently degraded database performance caused by crippled connection limits, administrators should calculate their semaphore requirements ahead of time:

  1. Take the expected max_connections value.
  2. Add 74 (assuming all other default settings remain unchanged).
  3. Divide that sum by 16 and round up to determine SEMMNI (the number of semaphore sets).
  4. Multiply that set count by 17 to determine SEMMNS (the total number of semaphores).
  5. Configure the system kernel parameters (kern.seminfo.semmni and kern.seminfo.semmns on OpenBSD; kern.ipc.semmni and kern.ipc.semmns on NetBSD) to at least double the computed figures, ensuring that other concurrent applications on the machine are not starved of synchronization resources.

By integrating num_os_semaphores directly into PostgreSQL 18, the development team has finally exorcised a twelve-year documentation ghost. While the ghost may be gone, the underlying physics of Unix kernel resource management remain—reminding administrators that even the most advanced database engines in the world are ultimately bound by the operating systems they run upon.