effective_io_concurrency is a request. io_max_concurrency is the grant.
New in PostgreSQL 18 as part of the asynchronous I/O subsystem, io_max_concurrency is the hard ceiling on how many I/O operations a single process can have in flight at once. Not per cluster; per process. The context is postmaster, so changing it requires a restart, and the default is -1, which tells PostgreSQL to pick a value at startup. The range runs to 1024.
The startup formula is worth knowing, because it has a failure mode. PostgreSQL takes shared_buffers in blocks, divides by every process the cluster could conceivably run (max_connections, autovacuum_worker_slots, max_worker_processes, max_wal_senders, plus the auxiliary processes), floors the result at 1, and caps it at 64. The reasoning is buffer pins: every in-flight read holds buffers pinned until it completes, and no process should be able to stake out more of shared_buffers than its fair share. On any normally provisioned server the division lands well above the cap, and you get 64:
1 SHOW io_max_concurrency; -- 64 (shared_buffers 128MB, max_connections 100)
Unlike some parameters we could name, SHOW reports the resolved value here, not the -1 you configured. The failure mode is what happens when the division doesn’t land above the cap. Same 128MB of shared_buffers with max_connections = 1000:
1 SHOW io_max_concurrency; -- 15
Drop shared_buffers to 16MB instead and it resolves to 11. The overprovisioned-max_connections anti-pattern already costs you memory, lock contention, and planning overhead; on PostgreSQL 18 it now also quietly strangles per-process I/O concurrency, and nothing in the log will tell you so.
The reason to care about the ceiling at all is its interaction with effective_io_concurrency. That parameter’s range runs to 1000, and tuning guides have spent years recommending 200 or more for SSDs. On 18, a process cannot hold more in-flight I/O than io_max_concurrency permits, whatever effective_io_concurrency asks for. Set effective_io_concurrency = 200 against the default ceiling of 64 and the extra 136 are aspiration. Anyone deliberately pushing I/O depth into the hundreds for storage that can genuinely absorb it (NVMe arrays, striped cloud volumes) needs to raise io_max_concurrency to match, restart included, or the tuning exercise is theater.
To see where a process actually sits against the ceiling, count its handles in pg_aios, the PostgreSQL 18 view of in-flight asynchronous I/O:
1 SELECT pid, count(*) AS in_flight FROM pg_aios GROUP BY pid;
A busy backend pinned at the limit while the storage has headroom is the one signature that justifies raising this parameter.
The recommendation follows from the formula. Leave it at -1 and it does the right thing on any sanely configured server. If it resolves to something small, the fix is almost never this parameter; it’s the shared_buffers that’s too small or the max_connections that’s too large, and those were problems before PostgreSQL 18 started charging for them here too. Raise it explicitly only in tandem with a deliberate effective_io_concurrency increase, and confirm the result in pg_aios rather than assuming the storage is grateful.