Five cartoon birds with halos and wings standing in a row on sand, with colored directional signs spelling "QUACK" above them and a white picket fence in the background.

num_os_semaphores exists because the PostgreSQL documentation kept getting an addition problem wrong, on and off, for twelve years. It is a preset: context internal, read-only, new in PostgreSQL 18. It reports how many operating-system semaphores the server asks for at startup, and on a default 18 installation the answer is 174.

If you run PostgreSQL on Linux or FreeBSD, nothing on your system limits that number and you can stop reading here. The rest is for macOS, NetBSD, OpenBSD, Solaris and illumos, and for anyone who enjoys watching a formula rot.

Most presets are compiled in (block_size, max_function_args). This one is computed at every startup. Every slot the server reserves for a backend or background process comes with one semaphore, which the process sleeps on while it waits for a lightweight lock, and the server creates all of them up front:

1max_connections + autovacuum_worker_slots + max_worker_processes + max_wal_senders + 40

The 40 is two singletons (the autovacuum launcher and the slot sync worker), six slots for auxiliary processes (checkpointer, background writer, WAL writer and the rest), and 32 slots for I/O workers. Those 32 are reserved no matter what io_method and io_workers say; io_method = sync does not give them back. Prepared transactions get no semaphores, so max_prepared_transactions is not in the sum.

A formula that would not stay fixed

Through version 17, the documentation told you to work this out yourself, with a formula that ended in an unexplained constant, which someone was supposed to update whenever a new kind of process appeared. Nobody did. In May 2024 Nathan Bossart went through the history: wrong from 9.2, when the checkpointer arrived, until someone noticed in 9.6; wrong again in 14, when the archiver became an auxiliary process; about to be wrong again in 17, which added the WAL summarizer. His fix was to stop publishing the sum and have the server report its own answer.

It is runtime-computed, like shared_memory_size, so you can ask for it without a running server:

1$ postgres -D $PGDATA -C num_os_semaphores
2174

That works even when the server cannot start, which is the only time most people will want it. It does not work while the server is up (postgres -C stops at the postmaster.pid lock file for runtime-computed parameters); use SHOW num_os_semaphores there.

One more turn of the wheel. Five months after this parameter went in, autovacuum_worker_slots did too, and it replaced autovacuum_max_workers in the sum. The documentation entry for num_os_semaphores still names autovacuum_max_workers, in 18 and in the 19 beta. On 18.6, autovacuum_max_workers = 10 leaves the value at 174; autovacuum_worker_slots = 32 moves it to 190. The number is right. The sentence describing it is stale.

If you are not on Linux

Since version 10, PostgreSQL on Linux and FreeBSD uses unnamed POSIX semaphores. Each is 128 bytes of the main shared memory segment (about 22kB for all 174), there is no kernel object behind it, and ipcs -s shows nothing. Windows has its own implementation.

The other Unix platforms use System V semaphores, and the kernel rations those. (macOS and Solaris ration generously enough that you will rarely notice, and PostgreSQL 19 moves Solaris to POSIX semaphores anyway. NetBSD and OpenBSD are another matter.) Version 18 allocates them in sets of 16, plus a 17th in each set holding a magic number so the server can recognize its own sets; current 17 releases use sets of 19 plus one. Two limits matter: SEMMNI, the number of sets, must be at least ceil(num_os_semaphores / 16), and SEMMNS, the total, at least that times 17. At the defaults that is 11 sets and 187 semaphores. Fall short and you get this, from a Linux build of 18.6 forced onto System V semaphores, with the kernel limited to 60 of them:

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

(ENOSPC is what semget() returns for this. The hint has clearly met its audience before.) With the limits at 11 and 187, the same server starts; at 11 and 186, it does not.

Sixty is not an arbitrary test value. It is the stock SEMMNS on NetBSD, and on OpenBSD through 7.9, and it is why 18 matters here. With default settings, 17.11 needs 129 semaphores and 18.6 needs 174; the extra 45 are the 32 I/O worker slots, and autovacuum_worker_slots defaulting to 16 where autovacuum_max_workers was 3. Version 17.11 can just squeeze under 60: initdb backs max_connections down to 25 and the server comes up using exactly 60. Version 18 cannot. The smallest configuration initdb will try needs 102, and the commit that settled the matter is titled “Give up on running with NetBSD/OpenBSD’s default semaphore settings.” The documentation now calls those defaults “unworkably small.”

OpenBSD has taken the hint. Its source tree raised the defaults in August 2026, to 25 sets and 350 semaphores, and the change should arrive with 8.0. Under those limits a default 18.6 starts, and keeps starting up to max_connections = 246. At 247 you are back to the FATAL above.

The failure is the good outcome. When the limits are tight but not hopeless, initdb does not fail; it tries max_connections of 100, 50, 40, 30 and 20 and keeps the first one that starts. With room for ten sets instead of eleven, I got a cluster with max_connections = 50 and autovacuum_worker_slots = 8, and the only notice was one line of initdb output:

1selecting default "max_connections" ... 50

So on a System V platform, raise the limits before initdb, not after the first outage. There is no cluster to ask at that point, so do the sum yourself: the max_connections you expect to need, plus 74 if the other three settings stay at their defaults. (Once a cluster exists, postgres -D $PGDATA -c max_connections=500 -C num_os_semaphores answers for a setting you have not made yet.) Divide by 16 and round up for SEMMNI, multiply that by 17 for SEMMNS, and set both (kern.seminfo.semmni and kern.seminfo.semmns on OpenBSD, kern.ipc.semmni and kern.ipc.semmns on NetBSD) to at least double what you computed, because PostgreSQL is not the only thing on the machine that wants semaphores.