Illustration of a print shop where humanoid ducks in maroon uniforms work among copying and printing machines alongside human employees processing orders.

max_replication_slots is the length of an array in shared memory, and that is all it is. It doesn’t decide who may create a slot, how much WAL a slot may pin, or when an abandoned one gets cleaned up. It decides how many entries the array has; the array is allocated once, at startup; and so the only interesting property of this number is that you cannot change it without a restart.

The default is 10, the context is postmaster, and the range is 0 to 262,143, which is MAX_BACKENDS. The source has carried a /* XXX? */ next to that upper bound since the parameter arrived in 9.4, and nobody has ever answered the question. Slots of any kind also need wal_level at replica or higher. From 9.4 through 9.6 the default was 0, so slots were opt-in and a restart away; PostgreSQL 10 raised it to 10 in the same commit that raised max_wal_senders to 10 and wal_level to replica, with the stated goal of letting a fresh install take a backup and grow a standby without restarting. That is the right way to read the default today: it is enough to get started. It is not a recommendation.

The entries cost nothing. On 18.6, postgres -C shared_memory_size reports the default as a rounding error, 10,000 entries as 3 MB more, and the full legal maximum of 262,143 as 72 MB more; call it 300 bytes each. (I mention the maximum only to make the point. If you have a quarter of a million replication slots you have a different problem, and I would like to hear about it.) The entry is cheap. The slot is not: a slot’s real cost is the WAL it pins and the xmin horizon it holds back, and that is max_slot_wal_keep_size’s department, with idle_replication_slot_timeout as the 18-era backstop. This post is about the seats, not what sits in them.

What takes a seat

Anything with a row in pg_replication_slots, regardless of type, state, or who created it. That is a longer list than the standbys and subscriptions you planned for:

  • One physical slot per standby that sets primary_slot_name, and one per pg_receivewal that was given a slot.
  • On a publisher, one logical slot per subscription, plus one more per table synchronization worker while an initial copy runs. Those are named pg_<subscription oid>_sync_<table oid>_<system identifier> and are dropped when the copy finishes, so a subscriber with the default max_sync_workers_per_subscription = 2 can hold three entries on the publisher while it catches up.
  • Temporary slots. pg_basebackup has created one by default since 10 (--no-slot turns it off), and wal_receiver_create_temp_slot (off by default) makes one for any standby without a permanent slot. They die with the session, but they occupy an entry while the session lives.
  • On a standby with sync_replication_slots = on, a copy of every failover slot on the primary.
  • Every slot a third party created: Debezium, the CDC pipeline, pg_recvlogical, that one person’s experiment from 2023.
  • Invalidated slots. A slot that blew through max_slot_wal_keep_size or sat idle past idle_replication_slot_timeout shows wal_status = 'lost' with an invalidation_reason, retains nothing, and still holds its entry until someone drops it. Invalidation stops the bleeding; it does not free the seat.

Not on the list, as of 18: replication origins. Through 17 this parameter also capped how many origins a subscriber could track, which is why older docs say it applies on the subscriber “with a different meaning”; 18 moved that job to max_active_replication_origins, so an 18 subscriber that publishes nothing and has no standby needs no slots at all. PostgreSQL 19 adds two wrinkles. A subscription with retain_dead_tuples = on has the launcher create a persistent physical slot named pg_conflict_detection on the subscriber, and that is an ordinary seat here. REPACK ... CONCURRENTLY, on the other hand, draws from its own pool, sized by the new max_repack_replication_slots (default 5) and carved out of the same array, so a rewrite can neither starve your replication nor be starved by it.

One short is the same as none

The error is all replication slots are in use, with the hint Free one or increase "max_replication_slots", and you will meet it in places where the hint is no help. Set the parameter to 0 and pg_basebackup fails with exactly that message, because the physical CREATE_REPLICATION_SLOT path in the walsender skips the “you have no slots at all” check the SQL functions run; there is nothing to free.

Two cases are worse than a failed command. On a publisher, the subscription’s own slot is created at CREATE SUBSCRIPTION; the table synchronization workers create theirs afterwards, as they start. With the publisher at max_replication_slots = 1, the subscription slot takes the only entry, and then every tablesync worker fails to create its slot, exits, and is respawned every wal_retrieve_retry_interval. The tables sit in pg_subscription_rel at srsubstate = 'd' indefinitely, the subscriber’s log repeats the error every five seconds, and the subscription itself looks fine: enabled, apply worker streaming, nothing copied. Raising the publisher’s limit (and restarting it) fixes this without touching the subscription.

The standby case is quieter. Failover slot synchronization, new in 17, needs the standby to hold a copy of every slot on the primary marked failover = true. On 18.6 with three such slots on the primary and a standby at max_replication_slots = 2, the slot sync worker created the first two as temporary slots, hit the limit on the third, raised the same error, and exited; and because it exited before persisting anything, its temporary slots went with it and the next attempt started from zero. The standby never synced a single slot. Two out of three is not what you get for being one short. You get nothing, on a loop, in a log nobody reads until the day of the failover.

The other direction is at least loud. Lower the value below the number of slots on disk and the server refuses to start: FATAL: too many replication slots active before shutdown, where “active” means “exists”. A slot nobody has touched in a year blocks startup exactly as a streaming one does. pg_upgrade applies the same rule when it migrates logical slots, which it does from 17 onward: the new cluster’s max_replication_slots must be at least the old cluster’s logical slot count, or --check stops there.

Setting it

Count what will be on this server after the next failover, not what is on it today: every physical standby, every subscription, max_sync_workers_per_subscription more per subscription for initial copies, one for pg_basebackup, whatever the CDC tooling wants, and the failover slots a standby will inherit. Then put the total away and set 50 on every node in the cluster, standbys included, because they will be primaries eventually and because slot synchronization needs the room now. Fifty entries cost 15 KB. If you are already using thirty, set 100. The arithmetic isn’t the point; the restart is. It is the expensive part of this parameter, and you want to pay it once, on your schedule, not at 3 a.m. because CREATE SUBSCRIPTION failed.

Two things travel with it. Every slot that is actually being consumed needs a walsender, so max_wal_senders wants to be at least this value plus your physical standbys; it has the same postmaster context, so raise them in the same restart. And watch the gap, not the count:

1SELECT count(*) AS slots,
2 count(*) FILTER (WHERE NOT active) AS inactive,
3 count(*) FILTER (WHERE wal_status = 'lost') AS invalidated,
4 current_setting('max_replication_slots')::int AS max
5 FROM pg_replication_slots;

Alert when slots gets within a handful of max, and treat any invalidated above zero as a slot to drop rather than a slot to keep; it protects nothing and occupies a seat. The seat is what this parameter controls, and seats are cheap. Everything expensive about a replication slot lives in the other columns of that view.