Subject: Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication

From: Ashutosh Sharma <[email-masked]>
Date: Thu, 26 Feb 2026 10:28:31 +0530

Hi,

On Wed, Feb 25, 2026 at 7:21 PM Ashutosh Sharma <[email-masked]> wrote:
>
> Hi Satya,
>
> On Wed, Feb 25, 2026 at 3:38 AM SATYANARAYANA NARLAPURAM
> <[email-masked]> wrote:
> >
> >
> > Hi hackers,
> >
> > synchronized_standby_slots requires that every physical slot listed in the GUC has caught up before a logical failover slot is allowed to proceed with decoding. This is an ALL-of-N slots  semantic.  The logical slot availability model does not align with quorum replication semantics set using synchronous_standby_names which can be configured for quorum commit (ANY M of N).
> >
> > In a typical 3 Node HA deployment with quorum sync rep:
> >
> > Primary, standby1 (corresponds to sb1_slot), standby2 (corresponds to sb2_slot)
> > synchronized_standby_slots = ' sb1_slot,  sb2_slot'
> > synchronous_standby_names = 'Any 1 ('standby1','standby2')'
> >
> > If standby1 goes down, synchronous commits still succeed because standby2 satisfies the quorum. However, logical decoding blocks indefinitely in WaitForStandbyConfirmation(), waiting for sb1_slot (corresponds to standby1) to catch up — even though the transaction is already safely committed on a quorum of synchronous standbys. This blocks logical decoding consumers from progressing and is inconsistent with the availability guarantee the DBA intended by choosing quorum commit.
>
> +1. This can indeed be a blocker for failover enabled logical
> replication. It not only has the potential to disrupt logical
> replication, but can also impact the primary server. Over time, it may
> silently lead to significant WAL accumulation on the primary,
> eventually causing disk-full scenarios and degrading the performance
> of applications running on the primary instance. Therefore, I too
> strongly believe this needs to be addressed to prevent such
> potentially disruptive situations.
>
> >
> >
> > Proposal:
> >
> > Make synchronized_standby_slots quorum aware i.e. extend the GUC to accept an ANY M (slot1, slot2, ...) syntax similar to synchronous_standby_names, so StandbySlotsHaveCaughtup() can return true when M of N slots (where M <= N and M >= 1) have caught up. I still prefer two different GUCs for this as the list of slots to be synchronized can still be different (for example, DBA may want to ensure Geo standby to be sync before allowing the logical decoding client to read the changes). I kept synchronized_standby_slots  parse logic similar to  synchronous_standby_names  to keep things simple. The default behavior is also not changed for  synchronized_standby_slots.
> >
>
> Thank you for the proposal. I can spend some time reviewing the
> changes and help take this forward. I would also be happy to hear
> others' thoughts and feedback on the proposal.
>

Thinking about this further, using quorum settings for
synchronized_standby_slots can/will certainly result in at least one
sync standby lagging behind the logical replica, making it probably
impossible to continue with the existing logical replication setup
after a failover to the standby that lags behind. Here is what I am
mean:

Let's say we have 2 synchronous standbys with
"synchronized_standby_slots" configured as ANY 1 (sync_standby1,
sync_standby2). With this quorum setting, WAL only needs to be
confirmed by any one of the two standbys before it can be forwarded to
the logical replica. Now consider a scenario where sync_standby1 is
ahead of sync_standby2, new WAL gets confirmed by sync_standby1 and
subsequently delivered to the logical replica. If sync_standby1 then
goes down and we failover to sync_standby2, the new primary will be at
a lower LSN than the logical replica, since sync_standby2 never
received that WAL. At this point, the logical replication slot on the
new primary is essentially stale, and the logical replication setup
that existed before the failover cannot be resumed. Hence, I think
it's important to ensure that the WAL (including all the necessary
data needed for logical replication) gets delivered to all the
servers/slots specified in synchronized_standby_slots before it gets
delivered to the logical replica.

While I agree that not allowing quorum like settings for this has the
potential to accumulate WAL and impact logical replication, I think we
can explore other ways to mitigate that concern separately.

Let's see what experts have to say on this.

--
With Regards,
Ashutosh Sharma.

---

From: Amit Kapila <[email-masked]>
Date: Thu, 26 Feb 2026 11:49:53 +0530

> > > Make synchronized_standby_slots quorum aware i.e. extend the GUC to accept an ANY M (slot1, slot2, ...) syntax similar to synchronous_standby_names, so StandbySlotsHaveCaughtup() can return tru…
...
> after a failover to the standby that lags behind. Here is what I am

But won't that be true even for synchronous_standby_names? I think in
the case of quorum, it is the responsibility of the failover solution
to select the most recent synced standby among all the standby's
specified in synchronous_standby_names. Similarly here before failing
over logical subscriber to one of physical standby, the failover tool
needs to ensure it is switching over to the synced replica. We have
given steps in the docs [1] that could be used to identify the replica
where the subscriber can switchover. Will that address your concern?

BTW, I have also suggested this idea in thread [2]. I don't recall all
the ideas/points discussed in that thread but it would be good to
check that thread for any alternative ideas and points raised, so that
we don't miss anything.

[1] - https://www.postgresql.org/docs/current/logical-replication-failover.html
[2] - https://www.postgresql.org/message-id/CAA4eK1KLFdmj8CLrZNL0D4phqyQihb7NXOjmqvrU5DT8moQn9Q%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Ashutosh Sharma <[email-masked]>
Date: Thu, 26 Feb 2026 13:12:06 +0530

Hi Amit,

> where the subscriber can switchover. Will that address your concern?

Here's my understanding of this:

I don't think we should be comparing "synchronous_standby_names" with
"synchronized_standby_slots", even though they appear similar in
purpose. All values listed in synchronous_standby_names represent
synchronous standbys exclusively, whereas synchronized_standby_slots
can hold values for both synchronous and asynchronous standbys. In
other words, every server referenced by synchronous_standby_names is
of the same type, but that may not be the case with
synchronized_standby_slots.

If a GUC can hold values of different types (sync vs. async), does it
really make sense to use a qualifier like ANY 1 (val1, val2) when val1
and val2 are different in nature? For example, suppose val1 is a
synchronous standby and val2 is an asynchronous standby, and we
configure ANY 1 (val1, val2). It's possible for val2 to get ahead of
val1 in terms of replication progress, which in turn could mean the
logical replica is also ahead of val1. So if we were to fail over to
val1 (since it's the only synchronous standby), we will not be able to
use the existing logical replication setup.

Please correct me if I have misunderstood anything here.

--
With Regards,
Ashutosh Sharma.

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Thu, 26 Feb 2026 00:02:56 -0800

Hi Amit,

> where the subscriber can switchover. Will that address your concern?

+1, the job of failover orchestration is to ensure the new primary is
caught up at least until the quorum LSN. Otherwise, it can be a durability
issue where users see missing committed transactions.

> check that thread for any alternative ideas and points raised, so that

Thanks for sharing the links,  the approach is similar. DEFAULT to
SAME_AS_SYNCREP_STANDBYS  is an interesting option.
I like the idea of avoiding duplicate lists unless the user wants to
maintain a separate list.

Thanks,
Satya

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Thu, 26 Feb 2026 00:23:45 -0800

Hi Ashutosh,

> use the existing logical replication setup.

If the failover orchestrator cannot ensure standby1 to not get the quorum
committed WAL (from archive or standby2) then the setting ANY 1 (val1,
val2) is invalid.
This setup also has issues because in your scenario, standby2 is ahead of
the new primary (standby1) and standby2 requires now to rewind to be in
sync with the new primary. Additionally, it allowed readers to read data
that was lost at the end of the failover. We ideally need a mechanism to
not send WAL to async replicas before the sync replicas commit  (honoring
syncrhnous_standby_names GUC) feature (similar to
synchronized_standby_slots). It could be a different thread on its own.

---

From: shveta malik <[email-masked]>
Date: Thu, 26 Feb 2026 14:15:12 +0530

On Thu, Feb 26, 2026 at 1:54 PM SATYANARAYANA NARLAPURAM
<[email-masked]> wrote:
>
> Hi Ashutosh,
>
> On Wed, Feb 25, 2026 at 11:42 PM Ashutosh Sharma <[email-masked]> wrote:
>>
>>
> If the failover orchestrator cannot ensure standby1 to not get the quorum committed WAL (from archive or standby2) then the setting ANY 1 (val1, val2) is invalid.
> This setup also has issues because in your scenario, standby2 is ahead of the new primary (standby1) and standby2 requires now to rewind to be in sync with the new primary. Additionally, it allowed readers to read data that was lost at the end of the failover. We ideally need a mechanism to not send WAL to async replicas before the sync replicas commit  (honoring syncrhnous_standby_names GUC) feature (similar to synchronized_standby_slots). It could be a different thread on its own.

+1 on the overall idea of the patch.
I understand the concern raised above that one of the standbys in the
quorum (synchronized_standby_slots) might lag behind the logical
replica, and a user could potentially failover to such a standby. But
I also agree with Amit that configuring failover correctly is
ultimately the responsibility of failover-solution. And instructions
in doc should be followed before deciding if a standby is
failover-ready or not.

As suggested in [1], IMO, it is a reasonably good idea for
'synchronized_standby_slots' to DEFAULT to the value of
'synchronous_standby_names'. That way, even if the user missed to
configure 'synchronized_standby_slots' explicitly, we would still have
reasonable protection in place. At the same time, if a user
intentionally chooses not to configure it, a NULL/NONE value should
remain a valid option.

[1]: https://www.postgresql.org/message-id/CAJpy0uCZ04ZQFHs-tV5LprkYtSSwtBtUJW4O%3D0S01yc%2BTRw7EQ%40mail.gmail.com

Thanks,
Shveta

---

From: Ashutosh Sharma <[email-masked]>
Date: Thu, 26 Feb 2026 14:41:42 +0530

Hi,

> intentionally chooses not to configure it, a NULL/NONE value should

AFAIU, not all names listed in "synchronous_standby_names" are
necessarily synchronous standbys. Tools like pg_receivewal, for
example, can establish a replication connection to the primary and
appear in that list. Therefore, deriving "synchronized_standby_slots"
from "synchronous_standby_names", if not set by the user would cause
logical slots to be synchronized to whatever nodes those names
represent, including a host running pg_receivewal, which is certainly
not something the user would have intended to do. Therefore I feel
this might not just be the good choice.

--
With Regards,
Ashutosh Sharma.

---

From: Alexander Kukushkin <[email-masked]>
Date: Thu, 26 Feb 2026 10:29:29 +0100

Hi,

On Thu, 26 Feb 2026 at 09:45, shveta malik <[email-masked]> wrote:

> reasonable protection in place.

Hmm.
synchronous_standby_names contains application_names,
while synchronized_standby_slots contains names of physical replication
slots.
These are two different things, and in fact sync replication doesn't even
require to use replication slots.
What is worse, even when all standbys use physical replication slots there
is no guarantee that values in synchronous_standby_names will match
physical slot names.

Regards,
--
Alexander Kukushkin

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Thu, 26 Feb 2026 02:38:44 -0800

Hi Alexnader,

On Thu, Feb 26, 2026 at 1:29 AM Alexander Kukushkin <[email-masked]>
wrote:

> physical slot names

That's right, thanks for reminding me. I am convinced that we can't use the
defaults of synchronous_standby_names for synchronized_standby_slots. What
do you think about the rest of the proposal?

Thanks,
Satya

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Thu, 26 Feb 2026 02:46:08 -0800

Hi Ashutosh,

On Thu, Feb 26, 2026 at 1:11 AM Ashutosh Sharma <[email-masked]>
wrote:

> > > If the failover orchestrator cannot ensure standby1 to not get the
> quorum committed WAL (from archive or standby2) then the setting ANY 1
> (val1, val2) is invalid.
> > > This setup also has issues because in your scenario, standby2 is ahead
> of the new primary (standby1) and standby2 requires now to rewind to be in

Agreed, not a good idea to have  synchronized_standby_slots default to
synchronous_standby_names because application_names and slot names are
different as stated.

Thanks,
Satya

---

From: Ashutosh Sharma <[email-masked]>
Date: Thu, 21 May 2026 14:42:15 +0530

Hi Shveta,

On Fri, May 15, 2026 at 9:28 AM shveta malik <[email-masked]> wrote:
>
> Ashutosh, while testing further, I noticed that
> 'synchronized_standby_slots' does not filter duplicate entries. As an
> example, if user ends up giving one entry twice in priority
> configuration, then we will end up waiting on one slot twice rather
> than waiting on 2 different slots.
>

Thank you for raising this concern. It is indeed an issue that needs
fixing. We will ensure it is addressed in the next patch version.

--
With Regards,
Ashutosh Sharma.

---

From: Ashutosh Sharma <[email-masked]>
Date: Wed, 3 Jun 2026 16:30:23 +0530

Hi Shveta,

On Fri, May 15, 2026 at 9:28 AM shveta malik <[email-masked]> wrote:
> Example:
> alter system set synchronized_standby_slots = 'FIRST 2 (standby_1,
> standby_1, standby_2, standby_3)';
> select pg_reload_conf();
> insert into tab1 values (10), (20), (30);
> select pg_logical_slot_get_binary_changes('sub1', NULL, NULL,
> 'proto_version', '4', 'publication_names', 'pub1');
>
> The last statement works even though standby_2 and standby_3 do not
> exist. It consumes standby_1 twice and thinks that the required number
> of slots has caught-up.
>
> OTOH, if we use the same configuration for
> 'synchronous_standby_names', it correctly waits for standby_2 and does
> not count on standby_1 twice.
>
> alter system set synchronous_standby_names = 'FIRST 2 (standby_1,
> standby_1, standby_2, standby_3)';
> insert into tab1 values (10), (20), (30);  ----> This will wait on standby_2
>
> This is perhaps because 'synchronous_standby_names ' waits on active
> WAL senders rather than repeated strings in configuration. But our
> code changes wait on the names present in 'synchronized_standby_slots'
> without filtering out duplicates.
>

May I know what your expectation is here? Would you like the check
hook for synchronized_standby_slots to automatically resolve
duplicates into a unique set of values, or should it detect duplicate
entries and raise an error so that the user can correct the
configuration?

If we automatically resolve duplicates, the user would still see the
GUC configured exactly as they specified, even though it would not
function the same way internally. For example, if a user sets:

FIRST 2 (s1, s1, s1, s2)

it might internally be resolved to:

FIRST 2 (s1, s2)

However, when the user runs SHOW, it would still display the original
configuration. This could give the user an incorrect impression of how
the setting is actually being interpreted. Because of this, I feel we
should treat duplicate entries as an invalid configuration and raise
an error.

As far as synchronous_standby_names is concerned, I can see that
configurations such as:

FIRST 2 (s1, s1, s1, s1)

are currently accepted, which I don't think is correct either and
should have been rejected, possibly resulted in the server startup
failure.

--
With Regards,
Ashutosh Sharma,

---

From: shveta malik <[email-masked]>
Date: Thu, 4 Jun 2026 09:13:52 +0530

> should have been rejected, possibly resulted in the server startup

My preference, and original intent, was to accept duplicate entries
and skip them internally. Doc can be updated to say 'duplicate entries
are skipped'. A server startup failure due to duplicate entries in a
GUC does not seem right to me. If the alter-system command fails due
to duplicate entries, that is still fine, but a startup failure seems
excessive. But let's see what others have to say on this.

thanks
Shveta

---

From: Ashutosh Sharma <[email-masked]>
Date: Thu, 4 Jun 2026 13:06:09 +0530

Hi,

> excessive. But let's see what others have to say on this.

Okay, the attached patch adds the capability to automatically remove
duplicate entries from the synchronized_standby_slots list. In N of M
mode, if N > M after removing duplicate entries, an error is raised.

This behavior has been documented, and test cases verifying the change
have been added.

A few other minor comments from [1] have also been addressed. Please
have a look at the attached patches with these changes.

[1] - https://www.postgresql.org/message-id/CAJpy0uCKGCkfCXCd%3DtsDH5e85x155LsdbZW46WpWfsZJUe82bw%40mail.gmail.com

--
With Regards,
Ashutosh Sharma.

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Thu, 4 Jun 2026 08:24:24 +0000

On Thursday, June 4, 2026 3:36 PM Ashutosh Sharma <[email-masked]> wrote:
> duplicate entries from the synchronized_standby_slots list.

Thanks for updating the patch.

I agree with Shveta that reporting an ERROR is not ideal. I also think it (ERROR) would
be inconsistent with existing GUCs, as most of them, such as
synchronous_standby_names, search_path, and session_preload_libraries, do not
enforce uniqueness.

The most similar GUC, synchronous_standby_names, also clarifies this in the
documentation:

	" There is no mechanism to enforce uniqueness of standby names. In case of
	duplicates one of the matching standbys will be considered as higher priority,
	though exactly which one is indeterminate."[1]

> In N of M

I'm not entirely sure about this case. It seems similar to when the number of
specified slots is less than N (in ANY N or FIRST N), given that we want to skip
duplicate slots. In that situation, the natural behavior to me would be to
simply block replication rather than raise an error. And
synchronous_standby_names would also simply block the transaction in this case.

[1] https://www.postgresql.org/docs/devel/runtime-config-replication.html#GUC-SYNCHRONOUS-STANDBY-NAMES

Best Regards,
Hou zj

---

From: Ashutosh Sharma <[email-masked]>
Date: Thu, 4 Jun 2026 14:56:59 +0530

Hi,

Thanks for your feedback.

> synchronous_standby_names would also simply block the transaction in this case.

For duplicate entries themselves, I agree with the direction of not
raising an error. Silently normalizing duplicates is reasonable for
this GUC, especially if we document it clearly. A repeated slot name
does not add any new information, so treating it as “same slot listed
twice by mistake” is practical.

But for N > M after deduplication, I would still lean toward raising an error.

Why I’d separate those cases:

1) Duplicate entries looks like a harmless normalization problem. ANY
2 (a, a, b) can be normalized to ANY 2 (a, b) without changing the
user’s apparent intent much.

2) N > M after deduplication is not a transient runtime state. ANY 2
(a, a) becomes one unique slot. That configuration can never succeed
unless the config itself changes. Blocking forever turns a static
configuration mistake into an operational liveness problem.

3) N > M after deduplication is different from ordinary “not enough
standbys are currently available”. If we configure ANY 2 (a, b) and
only a is currently caught up, blocking makes sense because the
situation may resolve at runtime. If we configure ANY 2 (a, a) and
duplicates are ignored, there is no possible future runtime in which
it succeeds without editing the GUC. That is why I think erroring is
better.

On the synchronous_standby_names comparison, I do not think it is
fully analogous. The quoted documentation is about there being no
reliable way to enforce uniqueness of standby names in the live
system, because those names are matched against runtime standbys and
the result can be indeterminate. Here, synchronized_standby_slots
names concrete replication slots, which are stable object identifiers.
Duplicate config entries are detectable and normalizable
deterministically at GUC parse time. That gives us a cleaner option
than synchronous_standby_names has.

So my preferred behavior would be:

1) duplicate names: normalize, do not error
2) after normalization, if num_sync > unique_slots: error immediately

--
With Regards,
Ashutosh Sharma.

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Fri, 5 Jun 2026 03:04:08 +0000

> than synchronous_standby_names has.

Thanks for the explanation.

What I was wondering is: ignoring duplicates, what should be the behavior of
"ANY 2 (standby)" when N > M?

I studied a bit for the behavior of synchronous_standby_names to understand the
difference. synchronous_standby_names does support syntax like "ANY 2 (standby)"
where N > M. Because even in that case, a transaction can still commit if there
are two standbys with the same name ("standby" in this example). I'm not sure
how common that use case is, but it may explain why no error is reported.

Given that, I'm not opposed to reporting an error in synchronized_standby_slots
when N > M. The situation is different here since there cannot be two slots with
the same name, making this a completely invalid use case.

Best Regards,
Hou zj

---

From: shveta malik <[email-masked]>
Date: Fri, 5 Jun 2026 09:06:32 +0530

> the same name, making this a completely invalid use case.

I also think, we can report error when N>M. IIRC, we were also
reporting earlier (without removing duplicates). Upon removing
duplicates, we can follow the same behaviour instead of walsender
being stuck indefinitely.

thanks
Shveta

---

From: shveta malik <[email-masked]>
Date: Fri, 5 Jun 2026 15:32:41 +0530

> 2) after normalization, if num_sync > unique_slots: error immediately

Thanks for tha pathces. I have attached a patch (txt file) with a few
trivial changes, take it if you find the changes acceptable.

thanks
Shveta
From bf2dd159b3de2f44cdb248b658f4c518ead98476 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email-masked]>
Date: Fri, 5 Jun 2026 15:28:45 +0530
Subject: [PATCH] top-up changes

---
 doc/src/sgml/config.sgml                      |  32 +++---
 src/backend/replication/slot.c                | 106 ++++++++++--------
 .../053_synchronized_standby_slots_quorum.pl  |  16 ---
 3 files changed, 80 insertions(+), 74 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7d8f4b75717..44276fe2bc0 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5225,19 +5225,6 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
          <literal>2 (slot1, slot2, slot3)</literal> and
          <literal>FIRST 2 (slot1, slot2, slot3)</literal> are equivalent.
        </para>
-        <para>
-         If the same physical replication slot name appears more than once,
-         duplicate entries are ignored and only the first occurrence is used.
-         The semantics of <varname>synchronized_standby_slots</varname> are
-         therefore based on the unique set of listed slot names, preserving the
-         original order of first occurrence. This means that, in
-         priority-based forms, duplicates do not create additional priority
-         positions: for example,
-         <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
-         same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>. In particular,
-         <replaceable class="parameter">num_sync</replaceable> must not exceed
-         the number of unique listed slots.
-        </para>
        <para>
         A plain comma-separated list without a keyword specifies that
         <emphasis>all</emphasis> listed physical slots must confirm WAL
@@ -5268,6 +5255,25 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
         (<literal>synchronous_standby_names = 'ANY ...'</literal>), so that
         logical decoding availability matches the commit durability guarantee.
        </para>
+        <para>
+         If the same physical replication slot name appears more than once,
+         duplicate entries are ignored and only the first occurrence is used.
+         The semantics of <varname>synchronized_standby_slots</varname> are
+         therefore based on the unique set of listed slot names, preserving the
+         original order of first occurrence. This means that, in
+         priority-based forms, duplicates do not create additional priority
+         positions: for example,
+         <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+         same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>.
+         Likewise, <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is
+         treated the same as <literal>ANY 2 (slot1, slot2, slot3)</literal>,
+         and a plain list such as <literal>(slot1, slot1, slot2)</literal>
+         is treated the same as <literal>(slot1, slot2)</literal>. In particular,
+         <replaceable class="parameter">num_sync</replaceable> must not exceed
+         the number of unique listed slots. Such a configuration results in an
+         error to prevent indefinite waits in WAL sender processes due to a
+         misconfigured <varname>synchronized_standby_slots</varname> setting.
+        </para>
         <para>
          <literal>FIRST</literal> and <literal>ANY</literal> are case-insensitive.
          If these keywords are used as the name of a replication slot,
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index fb45a1cd581..1e2e631d6a4 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -103,7 +103,7 @@ typedef struct
 	int			config_size;	/* total size of this struct, in bytes */
 	int			num_sync;		/* number of slots that must confirm WAL
 								 * receipt before logical decoding proceeds */
-	uint8		syncrep_method;	/* SYNC_REP_* method */
+	uint8		syncrep_method; /* SYNC_REP_* method */
 	int			nslotnames;		/* number of slot names that follow */
 
 	/*
@@ -117,12 +117,12 @@ typedef struct
  */
 typedef enum
 {
-	SS_SLOT_NOT_FOUND,		/* slot does not exist */
-	SS_SLOT_LOGICAL,		/* slot is logical, not physical */
-	SS_SLOT_INVALIDATED,	/* slot has been invalidated */
+	SS_SLOT_NOT_FOUND,			/* slot does not exist */
+	SS_SLOT_LOGICAL,			/* slot is logical, not physical */
+	SS_SLOT_INVALIDATED,		/* slot has been invalidated */
 	SS_SLOT_INACTIVE_LAGGING,	/* slot is inactive and behind wait_for_lsn */
-	SS_SLOT_ACTIVE_LAGGING,	/* slot is active and behind wait_for_lsn */
-} SyncStandbySlotsState;
+	SS_SLOT_ACTIVE_LAGGING,		/* slot is active and behind wait_for_lsn */
+}			SyncStandbySlotsState;
 
 /*
  * Information about a synchronized standby slot's state.
@@ -131,8 +131,9 @@ typedef struct
 {
 	const char *slot_name;		/* name of the slot */
 	SyncStandbySlotsState state;	/* state of the slot */
-	XLogRecPtr	restart_lsn;	/* current restart_lsn (valid for lagging states) */
-} SyncStandbySlotsStateInfo;
+	XLogRecPtr	restart_lsn;	/* current restart_lsn (valid for lagging
+								 * states) */
+}			SyncStandbySlotsStateInfo;
 
 /*
  * Lookup table for slot invalidation causes.
@@ -2994,16 +2995,20 @@ GetSlotInvalidationCauseName(ReplicationSlotInvalidationCause cause)
 }
 
 /*
- * Remove duplicate member names from a flat SyncRepConfigData in place.
+ * Remove duplicate member names from a SyncRepConfigData object.
  *
- * The first occurrence of each name is kept and input order is preserved.
+ * The member_names array of SyncRepConfigData is compacted in place so
+ * that only the first occurrence of each member name is retained. The
+ * original ordering of retained names is preserved, and nmembers and
+ * config_size are updated to describe only the compacted portion of
+ * the array.
  */
 static void
 CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
 {
 	char	   *src_name;
 	char	   *dst_name;
-	int			unique_members = 0;
+	int			nunique_members = 0;
 	Size		unique_size = offsetof(SyncRepConfigData, member_names);
 
 	src_name = config->member_names;
@@ -3011,38 +3016,47 @@ CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
 
 	for (int i = 0; i < config->nmembers; i++)
 	{
-		char	   *existing_name;
+		char	   *unique_name;
 		size_t		name_size;
 		bool		duplicate = false;
 
 		name_size = strlen(src_name) + 1;
-		existing_name = config->member_names;
 
-		for (int j = 0; j < unique_members; j++)
+		/*
+		 * Check whether src_name matches any previously retained unique name.
+		 * Only the first nunique_members entries in member_names need to be
+		 * examined for this.
+		 */
+		unique_name = config->member_names;
+		for (int j = 0; j < nunique_members; j++)
 		{
-			if (strcmp(existing_name, src_name) == 0)
+			if (strcmp(unique_name, src_name) == 0)
 			{
 				duplicate = true;
 				break;
 			}
 
-			existing_name += strlen(existing_name) + 1;
+			unique_name += strlen(unique_name) + 1;
 		}
 
 		if (!duplicate)
 		{
+			/*
+			 * This src_name is a new unique name. Copy it immediately after the
+			 * unique names retained so far.
+			 */
 			if (dst_name != src_name)
 				memmove(dst_name, src_name, name_size);
 
 			dst_name += name_size;
-			unique_members++;
+			nunique_members++;
 			unique_size += name_size;
 		}
 
 		src_name += name_size;
 	}
 
-	config->nmembers = unique_members;
+	config->nmembers = nunique_members;
 	config->config_size = (int) unique_size;
 }
 
@@ -3125,14 +3139,14 @@ check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
 			mname += strlen(mname) + 1;
 		}
 
-		/* Omit duplicate slot names so one slot is considered only once. */
+		/* Omit duplicate slot names to ensure each slot is considered only once. */
 		CompactSyncRepConfigMemberNames(syncrep_parse_result);
 
 		/*
 		 * For synchronized_standby_slots, a comma-separated list means all
 		 * listed slots are required. The syncrep parser preserves this shape
-		 * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce all-mode
-		 * semantics after removing duplicate names.
+		 * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce
+		 * all-mode semantics after removing duplicate names.
 		 */
 		if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
 			syncrep_parse_result->num_sync = syncrep_parse_result->nmembers;
@@ -3224,7 +3238,7 @@ SlotExistsInSyncStandbySlots(const char *slot_name)
  * catch-up requirement from being met.
  */
 static void
-ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states,
 								  int num_slot_states, int elevel,
 								  XLogRecPtr wait_for_lsn)
 {
@@ -3285,15 +3299,15 @@ ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
 							errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
 								   slot_name, "synchronized_standby_slots"),
 							errdetail("The slot's restart_lsn is not yet set; required LSN is %X/%X.",
-								  LSN_FORMAT_ARGS(wait_for_lsn)));
+									  LSN_FORMAT_ARGS(wait_for_lsn)));
 				else
 					ereport(DEBUG1,
 							errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 							errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
 								   slot_name, "synchronized_standby_slots"),
 							errdetail("The slot's restart_lsn %X/%X is behind the required %X/%X.",
-								  LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
-								  LSN_FORMAT_ARGS(wait_for_lsn)));
+									  LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
+									  LSN_FORMAT_ARGS(wait_for_lsn)));
 				break;
 
 			default:
@@ -3364,14 +3378,14 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
 	 * Determine how many slots are required and whether we're in "wait for
 	 * ALL" mode versus "wait for N-of-M" mode.
 	 *
-	 * wait_for_all = true means we need ALL slots to be ready (simple
-	 * list syntax like "slot1, slot2"). In this mode, we stop checking
-	 * on the first slot that is missing/invalid/logical, or the first slot
-	 * that is lagging (inactive or active).
+	 * wait_for_all = true means we need ALL slots to be ready (simple list
+	 * syntax like "slot1, slot2"). In this mode, we stop checking on the
+	 * first slot that is missing/invalid/logical, or the first slot that is
+	 * lagging (inactive or active).
 	 *
 	 * wait_for_all = false means we select N from M candidates (FIRST N or
-	 * ANY N syntax). In this mode, slots already caught up are counted even if
-	 * inactive. In FIRST N mode, we skip missing/invalid/logical slots and
+	 * ANY N syntax). In this mode, slots already caught up are counted even
+	 * if inactive. In FIRST N mode, we skip missing/invalid/logical slots and
 	 * lagging inactive slots, but wait for an active lagging slot with higher
 	 * priority. In ANY N mode, we skip lagging slots (inactive or active) to
 	 * find any N that have caught up. Duplicate configured slot names do not
@@ -3387,7 +3401,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
 	 * configured slots since in the worst case all could have problem states.
 	 */
 	slot_states = palloc_array(SyncStandbySlotsStateInfo,
-						  synchronized_standby_slots_config->nslotnames);
+							   synchronized_standby_slots_config->nslotnames);
 
 	/*
 	 * To prevent concurrent slot dropping and creation while filtering the
@@ -3396,8 +3410,8 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
 	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
 
 	/*
-	 * Iterate through configured slots, checking their state and tracking
-	 * how many have caught up. Problem states are recorded for deferred
+	 * Iterate through configured slots, checking their state and tracking how
+	 * many have caught up. Problem states are recorded for deferred
 	 * reporting: missing/logical/invalidated slots, and lagging slots
 	 * (inactive or active). Messages are only emitted if the catch-up
 	 * requirement isn't met.
@@ -3457,13 +3471,12 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
 		if (!XLogRecPtrIsValid(restart_lsn) || restart_lsn < wait_for_lsn)
 		{
 			/*
-			 * If a slot is inactive and lagging, report it as inactive.
-			 * If it is active and lagging, report it as lagging.
+			 * If a slot is inactive and lagging, report it as inactive. If it
+			 * is active and lagging, report it as lagging.
 			 *
-			 * In ALL mode: must wait for it.
-			 * In FIRST N (priority) mode: lagging active slots block, while
-			 * inactive slots can be skipped.
-			 * In ANY N (quorum) mode: skip and use another slot.
+			 * In ALL mode: must wait for it. In FIRST N (priority) mode:
+			 * lagging active slots block, while inactive slots can be
+			 * skipped. In ANY N (quorum) mode: skip and use another slot.
 			 */
 			slot_states[num_slot_states].slot_name = name;
 			slot_states[num_slot_states].state =
@@ -3497,12 +3510,12 @@ next_slot:
 	LWLockRelease(ReplicationSlotControlLock);
 
 	/*
-	 * If the required number of slots have not caught up, report any
-	 * recorded problem states and return false.
+	 * If the required number of slots have not caught up, report any recorded
+	 * problem states and return false.
 	 *
 	 * We only emit messages when the requirement is not met to avoid
-	 * misleading messages in quorum/priority mode where other slots may
-	 * have satisfied the condition despite some slots having issues.
+	 * misleading messages in quorum/priority mode where other slots may have
+	 * satisfied the condition despite some slots having issues.
 	 */
 	if (caught_up_slot_num < required)
 	{
@@ -3550,7 +3563,10 @@ WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
 			ProcessConfigFile(PGC_SIGHUP);
 		}
 
-		/* Exit once the configured synchronized_standby_slots requirement is met. */
+		/*
+		 * Exit once the configured synchronized_standby_slots requirement is
+		 * met.
+		 */
 		if (StandbySlotsHaveCaughtup(wait_for_lsn, WARNING))
 			break;
 
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
index 67a5b1d9657..be54b788807 100644
--- a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -15,22 +15,6 @@
 #
 #   synchronous_standby_names = 'ANY 1 (standby1, standby2)'
 #
-# Test scenarios:
-#
-# A) Plain list 'sb1_slot, sb2_slot' (ALL mode)
-#    - Works when all slots are available
-#    - Blocks immediately if ANY slot is unavailable
-#
-# B) ANY N (sb1_slot, sb2_slot, ...) (quorum mode)
-#    - Proceeds when at least N slots have caught up
-#    - Skips missing/invalid/logical slots and lagging slots (inactive or active)
-#      to find N caught-up slots
-#
-# C) FIRST N (sb1_slot, sb2_slot) (priority mode)
-#    - Selects first N slots in priority order (list order)
-#    - Skips missing/invalid/logical slots and inactive lagging slots,
-#      but waits for active lagging slots
-#    - FIRST 1 works with one slot down (unlike plain list)
 
 use strict;
 use warnings FATAL => 'all';
-- 
2.34.1

---

From: Ashutosh Sharma <[email-masked]>
Date: Mon, 8 Jun 2026 11:38:48 +0530

Hi,

> trivial changes, take it if you find the changes acceptable.

Thanks, I will review it and share my feedback.

--
With Regards,
Ashutosh Sharma.

---

From: Ashutosh Sharma <[email-masked]>
Date: Mon, 8 Jun 2026 14:23:28 +0530

Hi,

> Thanks, I will review it and share my feedback.

PFA the patches that incorporate the changes from the top-up patch
shared by Shveta in [1]. These changes primarily consist of
documentation updates, comment improvements, and indentation fixes at
a few places.

[1] - https://www.postgresql.org/message-id/CAJpy0uAdBxGpc4wtj-LcTGMNkVCYu4eMbDr27snEO_SrN2cV4A%40mail.gmail.com

--
With Regards,
Ashutosh Sharma.

---

From: Amit Kapila <[email-masked]>
Date: Mon, 8 Jun 2026 15:21:44 +0530

On Fri, Jun 5, 2026 at 9:06 AM shveta malik <[email-masked]> wrote:
> I also think, we can report error when N>M.
>

+1 for reporting an ERROR for this case.

-- 
With Regards,
Amit Kapila.

---

From: shveta malik <[email-masked]>
Date: Wed, 10 Jun 2026 12:15:52 +0530

Please find a few comments on June8 version fo patches:

1)
patch001:

SYNC_REP_DEFAULT: do we need to give one-line comment for this
somewhere as unlike PRIORITY and QUORUM, it is not self-explanatory.

patch002:
2)
It is better to avoid mentioning it as 'synchronized standby slots'.
We can make it as 'synchronized_standby_slots' in below comments:

+ /* Parse the synchronized standby slots configuration */

+ * Report problem states for synchronized standby slots that prevented the

3)
For these error-messages too, we  need to mention GUC name to give
better clarity.

+ GUC_check_errmsg("number of synchronized standby slots (%d) must not
exceed the number of unique listed slots (%d)",
+ syncrep_parse_result->num_sync,
+ syncrep_parse_result->nmembers);

How about:
GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
GUC_check_errmsg("invalid value for parameter \"%s\: synchronization
requirement (%d) exceeds the number of unique listed slots (%d)",
                 "synchronized_standby_slots",
                 syncrep_parse_result->num_sync,
                 syncrep_parse_result->nmembers);

Or
GUC_check_errmsg("invalid value for parameter \"%s\: required number
of synchronized standby slots (%d) exceeds the number of unique listed
slots (%d)",
                 "synchronized_standby_slots",
                 syncrep_parse_result->num_sync,
                 syncrep_parse_result->nmembers);

3)
+ GUC_check_errmsg("number of synchronized standby slots (%d) must be
greater than zero",
+ syncrep_parse_result->num_sync)

How about:
GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
GUC_check_errmsg("invalid value for parameter \"%s\: required number
of synchronized standby slots (%d) must be greater than zero",
                 "synchronized_standby_slots",
                 syncrep_parse_result->num_sync);

---

Or, better yet, we can split the messages and details for both, example:

GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
GUC_check_errmsg("invalid value for parameter \"%s\",
"synchronized_standby_slots");
GUC_check_errdetail("The required number of synchronized standby slots
(%d) exceeds the number of unique listed slots (%d)",
                     syncrep_parse_result->num_sync,
                    syncrep_parse_result->nmembers);)

4)
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states

We can get rid of space before slot_states. I think pgindent might
have put it in my patch. Sorry for the trouble.

5)
003:
- wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
+ wait_for_all =
+ (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);

This change can be moved to 002, right?

thanks
Shveta

---

From: Ashutosh Sharma <[email-masked]>
Date: Fri, 12 Jun 2026 15:40:23 +0530

Hi Shveta,

Thanks again for your review comments and suggestions. Please see my
comments inline below:

> somewhere as unlike PRIORITY and QUORUM, it is not self-explanatory.

Yes, it does makes sense to include a one-line comment. I've added it
in the attached patch.

> + * Report problem states for synchronized standby slots that prevented the

Good point. I've made the suggested change in the attached patch

>                  syncrep_parse_result->nmembers);

Updated as suggested in the attached patch.

>                  syncrep_parse_result->num_sync);

Updated as suggested in the attached patch.

> This change can be moved to 002, right?

Done.

PFA patch containing all the above changes.

--
With Regards,
Ashutosh Sharma.

---

From: Shlok Kyal <[email-masked]>
Date: Fri, 12 Jun 2026 18:33:32 +0530

Hi Ashutosh,

I have reviewed the patches. Here are some comments:

1. Should we update the doc for function "pg_logical_slot_get_changes". It says:
```
If the specified slot is a logical failover slot then the function will
not return until all physical slots specified in
<link linkend="guc-synchronized-standby-slots"><varname>synchronized_standby_slots</varname></link>
have confirmed WAL receipt.
```
This line "return until all physical slots specified in" seems wrong
after introduction of "FIRST/ANY"

2. Similarly, should we update the doc for function
"pg_replication_slot_advance"? It also says:
```
If the specified slot is a
logical failover slot then the function will not return until all
physical slots specified in
<link linkend="guc-synchronized-standby-slots"><varname>synchronized_standby_slots</varname></link>
have confirmed WAL receipt.
```

3. Comment in syncrep_gram.y states:
 * syncrep_gram.y - Parser for synchronous_standby_names

Since synchronosed_standby_slots is reusing this, should we update this comment?

4. Similarly for syncrep_scanner.l, we have comment:
 * syncrep_scanner.l
 *   a lexical scanner for synchronous_standby_names

 Should we update it?

5. enum "SyncStandbySlotsState" and stuct "SyncStandbySlotsStateInfo" should be
mentioned in typedefs.list, so that pgindent works properly.

Thanks,
Shlok Kyal

---

From: Ashutosh Sharma <[email-masked]>
Date: Mon, 15 Jun 2026 16:09:26 +0530

Hi Shlok,

Thanks for reviewing the patch and sharing your feedback - all your
comments are well noted. Please find my responses below.

> after introduction of "FIRST/ANY"

Agreed, it indeed needs correction, it has been addressed in the attached patch.

> have confirmed WAL receipt.

Same as comment 1, this has been corrected in the attached patch as well

> Since synchronosed_standby_slots is reusing this, should we update this comment?

Agreed, update was due. This has been taken care of in the attached patch.

>  *   a lexical scanner for synchronous_standby_names

Agreed and updated in the attached patch.

> mentioned in typedefs.list, so that pgindent works properly.

These have been added to the typdefs.list

PFA attached patches with all these changes.

--
With Regards,
Ashutosh Sharma

---

From: shveta malik <[email-masked]>
Date: Tue, 16 Jun 2026 09:43:19 +0530

Thanks for the patches.

1)
I see that there are 2 files now with 053*,  we shall move ours to 054.
ls t/053*
t/053_standby_login_event_trigger.pl
t/053_synchronized_standby_slots_quorum.pl

2)
I feel this test can be removed:
+# PART F: Plain list with first-prefixed slot name still means ALL mode

It is not a test for synchronized_standby_slots, IMO, it is testing
the syncrep parser. Or let me know if my understanding is wrong?

thanks
Shveta

---

From: Shlok Kyal <[email-masked]>
Date: Tue, 16 Jun 2026 12:09:47 +0530

Hi Ashutosh,

I checked the CFbot and saw the test was failing [1].

It is failing for the test:
not ok 4     - guc_privs                                 171 ms

diff -U3 /__w/postgresql/postgresql/src/test/modules/unsafe_tests/expected/guc_privs.out
/__w/postgresql/postgresql/build/testrun/unsafe_tests/regress/results/guc_privs.out
--- /__w/postgresql/postgresql/src/test/modules/unsafe_tests/expected/guc_privs.out
2026-06-15 11:08:26.979460240 +0000
+++ /__w/postgresql/postgresql/build/testrun/unsafe_tests/regress/results/guc_privs.out
2026-06-15 11:20:01.829423596 +0000
@@ -590,8 +590,7 @@
 -- Cannot set synchronized_standby_slots to an invalid slot name
 ALTER SYSTEM SET synchronized_standby_slots='invalid*';
 ERROR:  invalid value for parameter "synchronized_standby_slots": "invalid*"
-DETAIL:  replication slot name "invalid*" contains invalid character
-HINT:  Replication slot names may only contain lower case letters,
numbers, and the underscore character.
+DETAIL:  syntax error at or near "*"
 -- Can set synchronized_standby_slots to a non-existent slot name
 ALTER SYSTEM SET synchronized_standby_slots='missing';
 -- Reset the GUC

In HEAD, validate_sync_standby_slots() called SplitIdentifierString()
and then ReplicationSlotValidateNameInternal() for each element, which
produced the slot-name-specific error for inputs such as 'invalid*'.
With patch synchronized_standby_slots validation use the syncrep
parser and a syntax error is reported instead.
I think we should update the corresponding expected file.

[1]: https://github.com/postgresql-cfbot/postgresql/actions/runs/27541996426/job/81406065089

Thanks,
Shlok Kyal

---

From: Ashutosh Sharma <[email-masked]>
Date: Tue, 16 Jun 2026 16:12:43 +0530

Hi,

> not ok 4     - guc_privs                                 171 ms

Thanks for reporting this. The attached patches address the issue and
also incorporate the feedback from Shevta in [1].

[1] - https://www.postgresql.org/message-id/CAJpy0uCKaqD-UfCX%3DQ5WkdtxUS8ZCJwLduegi5Etdx7gkV-02g%40mail.gmail.com

--
With Regards,
Ashutosh Sharma.