Hello Hackers,

This patch introduces a new postmaster-level configuration parameter,
max_logical_replication_slots, which limits the number of logical
replication slots that can be created.

Currently, max_replication_slots governs the total number of slots,
but there's no separate limit for logical slots. This patch:

Adds max_logical_replication_slots GUC, defaulting to -1
(falls back to max_replication_slots).

Enforces at server startup that max_logical_replication_slots ≤
max_replication_slots. PostgreSQL will refuse to start if this
is violated or if there are more existing logical slots than
the configured maximum.

Checks the logical slot limit when creating new slots at runtime,
preventing creation beyond the configured maximum.

Updates documentation, sample config, and test_decoding tests
to include logical slot limits.

This provides a separation between logical and total replication
slots, and allows users to control logical slot usage independently.

Best regards,

--
Ahmed Et-tanany
Aiven: https://aiven.io/
From 845b61ec963145ed66719c9529735a276518afc0 Mon Sep 17 00:00:00 2001
From: Ahmed Et-tanany <[email protected]>
Date: Tue, 27 Jan 2026 12:15:46 +0100
Subject: [PATCH] Add max_logical_replication_slots GUC and enforce startup
 invariant

Introduce a new postmaster-level configuration parameter,
max_logical_replication_slots, to limit the number of logical
replication slots that can exist simultaneously.

The parameter defaults to -1, meaning it falls back to
max_replication_slots. Setting max_logical_replication_slots greater
than max_replication_slots is prohibited.

Enforce this invariant early during server startup in
StartupReplicationSlots(), before restoring slots from disk. If the
number of existing logical slots exceeds the configured maximum, the
server will fail to start with a FATAL error. This prevents confusing
situations where too many logical slots exist on disk.

Additionally, enforce the logical slot limit when creating new
slots at runtime, preventing more logical slots than the configured
maximum.

Add relevant documentation, sample configuration, and test cases
for verifying the new GUC and its enforcement.
---
 contrib/test_decoding/expected/slot.out       | 87 ++++++++++++++++++-
 contrib/test_decoding/logical.conf            |  3 +-
 contrib/test_decoding/sql/slot.sql            | 25 +++++-
 doc/src/sgml/config.sgml                      | 22 +++++
 doc/src/sgml/logical-replication.sgml         |  6 ++
 src/backend/replication/slot.c                | 48 ++++++++++
 src/backend/utils/misc/guc_parameters.dat     |  8 ++
 src/backend/utils/misc/postgresql.conf.sample |  3 +
 src/include/replication/slot.h                |  1 +
 9 files changed, 198 insertions(+), 5 deletions(-)

diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out
index 7de03c79f6f..94f0d4bafd3 100644
--- a/contrib/test_decoding/expected/slot.out
+++ b/contrib/test_decoding/expected/slot.out
@@ -216,11 +216,11 @@ ORDER BY o.slot_name, c.slot_name;
  orig_slot1 | test_decoding | f         | copied_slot1_no_change          | test_decoding | f
 (3 rows)
 
--- Now we have maximum 4 replication slots. Check slots are properly
+-- Now we have maximum 4 logical replication slots. Check slots are properly
 -- released even when raise error during creating the target slot.
 SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
-ERROR:  all replication slots are in use
-HINT:  Free one or increase "max_replication_slots".
+ERROR:  all logical replication slots are in use
+HINT:  Free one or increase "max_logical_replication_slots".
 -- temporary slots were dropped automatically
 SELECT pg_drop_replication_slot('orig_slot1');
  pg_drop_replication_slot 
@@ -466,3 +466,84 @@ SELECT pg_drop_replication_slot('physical_slot');
  
 (1 row)
 
+--
+-- Test maximum limits for replication slots
+--
+-- Check that no more than 4 logical replication slots can be created
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot1', 'test_decoding');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot2', 'test_decoding');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot3', 'test_decoding');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot4', 'test_decoding');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot5', 'test_decoding'); -- error
+ERROR:  all logical replication slots are in use
+HINT:  Free one or increase "max_logical_replication_slots".
+-- Check that the remaining 2 slots can be used for physical replication
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot1');
+ ?column? 
+----------
+ init
+(1 row)
+
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot2'); -- error
+ERROR:  all replication slots are in use
+HINT:  Free one or increase "max_replication_slots".
+SELECT slot_name, slot_type FROM pg_replication_slots;
+   slot_name    | slot_type 
+----------------+-----------
+ logical_slot1  | logical
+ logical_slot2  | logical
+ logical_slot3  | logical
+ logical_slot4  | logical
+ physical_slot1 | physical
+(5 rows)
+
+SELECT pg_drop_replication_slot('logical_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('logical_slot2');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('logical_slot3');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('logical_slot4');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_drop_replication_slot('physical_slot1');
+ pg_drop_replication_slot 
+--------------------------
+ 
+(1 row)
+
diff --git a/contrib/test_decoding/logical.conf b/contrib/test_decoding/logical.conf
index cc12f2542b4..61ea6ef3fdb 100644
--- a/contrib/test_decoding/logical.conf
+++ b/contrib/test_decoding/logical.conf
@@ -1,4 +1,5 @@
 wal_level = logical
-max_replication_slots = 4
+max_replication_slots = 5
+max_logical_replication_slots = 4
 logical_decoding_work_mem = 64kB
 autovacuum_naptime = 1d
diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql
index 580e3ae3bef..774936113d4 100644
--- a/contrib/test_decoding/sql/slot.sql
+++ b/contrib/test_decoding/sql/slot.sql
@@ -103,7 +103,7 @@ WHERE
     o.slot_name != c.slot_name
 ORDER BY o.slot_name, c.slot_name;
 
--- Now we have maximum 4 replication slots. Check slots are properly
+-- Now we have maximum 4 logical replication slots. Check slots are properly
 -- released even when raise error during creating the target slot.
 SELECT 'copy' FROM pg_copy_logical_replication_slot('orig_slot1', 'failed'); -- error
 
@@ -190,3 +190,26 @@ SELECT pg_drop_replication_slot('failover_true_slot');
 SELECT pg_drop_replication_slot('failover_false_slot');
 SELECT pg_drop_replication_slot('failover_default_slot');
 SELECT pg_drop_replication_slot('physical_slot');
+
+--
+-- Test maximum limits for replication slots
+--
+
+-- Check that no more than 4 logical replication slots can be created
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot1', 'test_decoding');
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot2', 'test_decoding');
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot3', 'test_decoding');
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot4', 'test_decoding');
+SELECT 'init' FROM pg_create_logical_replication_slot('logical_slot5', 'test_decoding'); -- error
+
+-- Check that the remaining 2 slots can be used for physical replication
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot1');
+SELECT 'init' FROM pg_create_physical_replication_slot('physical_slot2'); -- error
+
+SELECT slot_name, slot_type FROM pg_replication_slots;
+
+SELECT pg_drop_replication_slot('logical_slot1');
+SELECT pg_drop_replication_slot('logical_slot2');
+SELECT pg_drop_replication_slot('logical_slot3');
+SELECT pg_drop_replication_slot('logical_slot4');
+SELECT pg_drop_replication_slot('physical_slot1');
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5560b95ee60..ec3768f302f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -4575,6 +4575,28 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </listitem>
       </varlistentry>
 
+      <varlistentry id="guc-max-logical-replication-slots" xreflabel="max_logical_replication_slots">
+       <term><varname>max_logical_replication_slots</varname> (<type>integer</type>)
+       <indexterm>
+        <primary><varname>max_logical_replication_slots</varname> configuration parameter</primary>
+       </indexterm>
+       </term>
+       <listitem>
+        <para>
+         Specifies the maximum number of logical replication slots that
+         the server can support.  It defaults to -1, indicating that
+         the value of <xref linkend="guc-max-replication-slots"/> should
+         be used instead.  The value of this parameter cannot exceed
+         <xref linkend="guc-max-replication-slots"/>, and the total number
+         of replication slots (logical and physical) is limited by
+         <varname>max_replication_slots</varname>.  This parameter can
+         only be set at server start.  Setting it to a lower value than
+         the number of currently existing logical replication slots will
+         prevent the server from starting.
+        </para>
+       </listitem>
+      </varlistentry>
+
       <varlistentry id="guc-wal-keep-size" xreflabel="wal_keep_size">
        <term><varname>wal_keep_size</varname> (<type>integer</type>)
        <indexterm>
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 5028fe9af09..86d00f48a21 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2641,6 +2641,12 @@ CONTEXT:  processing remote data for replication origin "pg_16395" during "INSER
     plus some reserve for table synchronization.
    </para>
 
+   <para>
+    <link linkend="guc-max-logical-replication-slots"><varname>max_logical_replication_slots</varname></link>
+    must be set to at least the number of subscriptions expected to connect,
+    plus some reserve for table synchronization.
+   </para>
+
    <para>
     Logical replication slots are also affected by
     <link linkend="guc-idle-replication-slot-timeout"><varname>idle_replication_slot_timeout</varname></link>.
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 4c47261c7f9..42865c71ebb 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -150,6 +150,8 @@ ReplicationSlot *MyReplicationSlot = NULL;
 /* GUC variables */
 int			max_replication_slots = 10; /* the maximum number of replication
 										 * slots */
+int			max_logical_replication_slots = -1; /* the maximum number of
+												 * logical replication slots */
 
 /*
  * Invalidate replication slots that have remained idle longer than this
@@ -381,6 +383,9 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 {
 	ReplicationSlot *slot = NULL;
 	int			i;
+	int			used_logical_slot_count = 0;
+	int			max_logical_slots = max_logical_replication_slots != -1 ?
+		max_logical_replication_slots : max_replication_slots;
 
 	Assert(MyReplicationSlot == NULL);
 
@@ -442,6 +447,8 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_OBJECT),
 					 errmsg("replication slot \"%s\" already exists", name)));
+		if (s->in_use && SlotIsLogical(s))
+			used_logical_slot_count++;
 		if (!s->in_use && slot == NULL)
 			slot = s;
 	}
@@ -454,6 +461,17 @@ ReplicationSlotCreate(const char *name, bool db_specific,
 				 errmsg("all replication slots are in use"),
 				 errhint("Free one or increase \"max_replication_slots\".")));
 
+	/*
+	 * Check the logical replication slots limit. Any remaining slots are to
+	 * be used for physical replication.
+	 */
+	if (db_specific &&
+		used_logical_slot_count >= max_logical_slots)
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+				 errmsg("all logical replication slots are in use"),
+				 errhint("Free one or increase \"max_logical_replication_slots\".")));
+
 	/*
 	 * Since this slot is not in use, nobody should be looking at any part of
 	 * it other than the in_use field unless they're trying to allocate it.
@@ -2379,9 +2397,19 @@ StartupReplicationSlots(void)
 {
 	DIR		   *replication_dir;
 	struct dirent *replication_de;
+	int			i;
+	int			logical_slot_count = 0;
 
 	elog(DEBUG1, "starting up replication slots");
 
+	/* fail early for invalid GUCs */
+	if (max_logical_replication_slots != -1 &&
+		max_logical_replication_slots > max_replication_slots)
+		ereport(FATAL,
+				(errmsg("max_logical_replication_slots (%d) cannot be greater than max_replication_slots (%d)",
+						max_logical_replication_slots,
+						max_replication_slots)));
+
 	/* restore all slots by iterating over all on-disk entries */
 	replication_dir = AllocateDir(PG_REPLSLOT_DIR);
 	while ((replication_de = ReadDir(replication_dir, PG_REPLSLOT_DIR)) != NULL)
@@ -2419,6 +2447,26 @@ StartupReplicationSlots(void)
 	}
 	FreeDir(replication_dir);
 
+	/* check that we don't have more logical slots restored than allowed */
+	if (max_logical_replication_slots != -1)
+	{
+		for (i = 0; i < max_replication_slots; i++)
+		{
+			ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[i];
+
+			if (!slot->in_use)
+				continue;
+
+			if (SlotIsLogical(slot))
+				logical_slot_count++;
+		}
+
+		if (logical_slot_count > max_logical_replication_slots)
+			ereport(FATAL,
+					(errmsg("too many logical replication slots active before shutdown"),
+					 errhint("Increase \"max_logical_replication_slots\" and try again.")));
+	}
+
 	/* currently no slots exist, we're done. */
 	if (max_replication_slots <= 0)
 		return;
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index f0260e6e412..f99b3cbf436 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1928,6 +1928,14 @@
   max => 'INT_MAX',
 },
 
+{ name => 'max_logical_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING',
+  short_desc => 'Sets the maximum number of simultaneously defined logical replication slots.',
+  variable => 'max_logical_replication_slots',
+  boot_val => '-1',
+  min => '-1',
+  max => 'MAX_BACKENDS /* XXX? */',
+},
+
 { name => 'max_logical_replication_workers', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SUBSCRIBERS',
   short_desc => 'Maximum number of logical replication worker processes.',
   variable => 'max_logical_replication_workers',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c4f92fcdac8..5654b97a6fa 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -342,6 +342,9 @@
                                 # (change requires restart)
 #wal_keep_size = 0              # in megabytes; 0 disables
 #max_slot_wal_keep_size = -1    # in megabytes; -1 disables
+#max_logical_replication_slots = -1     # max number of logical replication slots,
+                                        # -1 means use max_replication_slots
+                                        # (change requires restart)
 #idle_replication_slot_timeout = 0      # in seconds; 0 disables
 #wal_sender_timeout = 60s       # in milliseconds; 0 disables
 #track_commit_timestamp = off   # collect timestamp of transaction commit
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index f465e430cc6..ae4abad4371 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -321,6 +321,7 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot;
 
 /* GUCs */
 extern PGDLLIMPORT int max_replication_slots;
+extern PGDLLIMPORT int max_logical_replication_slots;
 extern PGDLLIMPORT char *synchronized_standby_slots;
 extern PGDLLIMPORT int idle_replication_slot_timeout_secs;
 
-- 
2.52.0

Reply via email to