From 55f90cff5a3cf0579eb3b8fe65cd6d43ba054051 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Mon, 18 Aug 2025 09:05:38 +0530
Subject: [PATCH v20250818 5/6] New worker for sequence synchronization during
 subscription management

This patch introduces sequence synchronization:
Sequences have 2 states:
   - INIT (needs synchronizing)
   - READY (is already synchronized)

A new sequencesync worker is launched as needed to synchronize sequences.
It does the following:
    a) Retrieves remote values of sequences with pg_sequence_state() INIT.
    b) Logs a warning if the sequence parameters differ between the publisher and subscriber.
    c) Sets the local sequence values accordingly.
    d) Updates the local sequence state to READY.
    e) Repeats until all done; Commits synchronized sequences in batches of 100

Sequence synchronization occurs in 3 places:
1) CREATE SUBSCRIPTION
    - (PG19 command syntax is unchanged)
    - The subscriber retrieves sequences associated with publications.
    - Published sequences are added to pg_subscription_rel with INIT state.
    - Initiate the sequencesync worker (see above) to synchronize all
      sequences.

2) ALTER SUBSCRIPTION ... REFRESH PUBLICATION
    - (PG19 command syntax is unchanged)
    - Dropped published sequences are removed from pg_subscription_rel.
    - Newly published sequences are added to pg_subscription_rel with INIT state.
    - Initiate the sequencesync worker (see above) to synchronize only
      newly added sequences.

3) ALTER SUBSCRIPTION ... REFRESH PUBLICATION SEQUENCES
    - The patch introduces this new command to refresh all sequences
    - Dropped published sequences are removed from pg_subscription_rel.
    - Newly published sequences are added to pg_subscription_rel
    - All sequences in pg_subscription_rel are reset to INIT state.
    - Initiate the sequencesync worker (see above) to synchronize all
      sequences.

Author: Vignesh C
Reviewer:  Amit Kapila, Shveta Malik, Dilip Kumar, Peter Smith, Nisha Moond
Discussion: https://www.postgresql.org/message-id/CAA4eK1LC+KJiAkSrpE_NwvNdidw9F2os7GERUeSxSKv71gXysQ@mail.gmail.com
---
 src/backend/catalog/pg_subscription.c         |   2 +-
 src/backend/catalog/system_views.sql          |   1 +
 src/backend/commands/sequence.c               |  29 +-
 src/backend/commands/subscriptioncmds.c       |   9 +-
 src/backend/postmaster/bgworker.c             |   5 +-
 src/backend/replication/logical/Makefile      |   1 +
 src/backend/replication/logical/launcher.c    |  59 +-
 src/backend/replication/logical/meson.build   |   1 +
 .../replication/logical/sequencesync.c        | 630 ++++++++++++++++++
 src/backend/replication/logical/syncutils.c   | 125 +++-
 src/backend/replication/logical/tablesync.c   |  88 +--
 src/backend/replication/logical/worker.c      |  69 +-
 .../utils/activity/pgstat_subscription.c      |  27 +-
 src/backend/utils/adt/pgstatfuncs.c           |  27 +-
 src/backend/utils/misc/guc_tables.c           |   2 +-
 src/include/catalog/pg_proc.dat               |   6 +-
 src/include/catalog/pg_subscription_rel.h     |   9 +
 src/include/commands/sequence.h               |   3 +
 src/include/pgstat.h                          |   6 +-
 src/include/replication/logicalworker.h       |   3 +-
 src/include/replication/worker_internal.h     |  29 +-
 src/test/regress/expected/rules.out           |   3 +-
 src/test/subscription/meson.build             |   1 +
 src/test/subscription/t/026_stats.pl          |  58 +-
 src/test/subscription/t/036_sequences.pl      | 239 +++++++
 src/tools/pgindent/typedefs.list              |   1 +
 26 files changed, 1266 insertions(+), 167 deletions(-)
 create mode 100644 src/backend/replication/logical/sequencesync.c
 create mode 100644 src/test/subscription/t/036_sequences.pl

diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 28622e54aaa..383f01f83e2 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -352,7 +352,7 @@ UpdateSubscriptionRelState(Oid subid, Oid relid, char state,
 							  ObjectIdGetDatum(relid),
 							  ObjectIdGetDatum(subid));
 	if (!HeapTupleIsValid(tup))
-		elog(ERROR, "subscription table %u in subscription %u does not exist",
+		elog(ERROR, "subscription relation %u in subscription %u does not exist",
 			 relid, subid);
 
 	/* Update the tuple. */
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 0b05b879ca8..f4da3bc54b6 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1408,6 +1408,7 @@ CREATE VIEW pg_stat_subscription_stats AS
         ss.subid,
         s.subname,
         ss.apply_error_count,
+        ss.sequence_sync_error_count,
         ss.sync_error_count,
         ss.confl_insert_exists,
         ss.confl_update_origin_differs,
diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c
index 467947dce8b..d07b718fd83 100644
--- a/src/backend/commands/sequence.c
+++ b/src/backend/commands/sequence.c
@@ -112,7 +112,6 @@ static void init_params(ParseState *pstate, List *options, bool for_identity,
 						bool *is_called,
 						bool *need_seq_rewrite,
 						List **owned_by);
-static void do_setval(Oid relid, int64 next, bool iscalled);
 static void process_owned_by(Relation seqrel, List *owned_by, bool for_identity);
 
 
@@ -954,9 +953,12 @@ lastval(PG_FUNCTION_ARGS)
  * restore the state of a sequence exactly during data-only restores -
  * it is the only way to clear the is_called flag in an existing
  * sequence.
+ *
+ * log_cnt is currently used only by the sequence syncworker to set the
+ * log_cnt for sequences while synchronizing values from the publisher.
  */
-static void
-do_setval(Oid relid, int64 next, bool iscalled)
+void
+SetSequence(Oid relid, int64 next, int64 log_cnt, bool is_called)
 {
 	SeqTable	elm;
 	Relation	seqrel;
@@ -1007,7 +1009,7 @@ do_setval(Oid relid, int64 next, bool iscalled)
 						minv, maxv)));
 
 	/* Set the currval() state only if iscalled = true */
-	if (iscalled)
+	if (is_called)
 	{
 		elm->last = next;		/* last returned number */
 		elm->last_valid = true;
@@ -1024,8 +1026,8 @@ do_setval(Oid relid, int64 next, bool iscalled)
 	START_CRIT_SECTION();
 
 	seq->last_value = next;		/* last fetched number */
-	seq->is_called = iscalled;
-	seq->log_cnt = 0;
+	seq->is_called = is_called;
+	seq->log_cnt = log_cnt;
 
 	MarkBufferDirty(buf);
 
@@ -1057,7 +1059,7 @@ do_setval(Oid relid, int64 next, bool iscalled)
 
 /*
  * Implement the 2 arg setval procedure.
- * See do_setval for discussion.
+ * See SetSequence for discussion.
  */
 Datum
 setval_oid(PG_FUNCTION_ARGS)
@@ -1065,14 +1067,14 @@ setval_oid(PG_FUNCTION_ARGS)
 	Oid			relid = PG_GETARG_OID(0);
 	int64		next = PG_GETARG_INT64(1);
 
-	do_setval(relid, next, true);
+	SetSequence(relid, next, SEQ_LOG_CNT_INVALID, true);
 
 	PG_RETURN_INT64(next);
 }
 
 /*
  * Implement the 3 arg setval procedure.
- * See do_setval for discussion.
+ * See SetSequence for discussion.
  */
 Datum
 setval3_oid(PG_FUNCTION_ARGS)
@@ -1081,7 +1083,7 @@ setval3_oid(PG_FUNCTION_ARGS)
 	int64		next = PG_GETARG_INT64(1);
 	bool		iscalled = PG_GETARG_BOOL(2);
 
-	do_setval(relid, next, iscalled);
+	SetSequence(relid, next, SEQ_LOG_CNT_INVALID, iscalled);
 
 	PG_RETURN_INT64(next);
 }
@@ -1847,6 +1849,13 @@ pg_get_sequence_data(PG_FUNCTION_ARGS)
 		values[0] = Int64GetDatum(seq->last_value);
 		values[1] = BoolGetDatum(seq->is_called);
 		values[2] = Int64GetDatum(seq->log_cnt);
+
+		/*
+		 * The page LSN will be used in logical replication of sequences to
+		 * record the LSN of the sequence page in the pg_subscription_rel
+		 * system catalog.  It reflects the LSN of the remote sequence at the
+		 * time it was synchronized.
+		 */
 		values[3] = LSNGetDatum(PageGetLSN(page));
 
 		UnlockReleaseBuffer(buf);
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 16ea1ed1fb1..9de23aede3d 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -876,7 +876,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
  *     Add or remove tables and sequences that have been added to or removed
  * 	   from the publication since the last subscription creation or refresh.
  * If 'resync_all_sequences' is true:
- *     Perform the above operation only for sequences.
+ *     Perform the above operation only for sequences and resync all the
+ *     sequences including existing ones.
  *
  * Note, this is a common function for handling different REFRESH commands
  * according to the parameter 'resync_all_sequences'
@@ -1030,7 +1031,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 				if (resync_all_sequences)
 				{
 					UpdateSubscriptionRelState(sub->oid, relid, SUBREL_STATE_INIT,
-											   InvalidXLogRecPtr);
+											   InvalidXLogRecPtr, false);
 					ereport(DEBUG1,
 							errmsg_internal("sequence \"%s.%s\" of subscription \"%s\" set to INIT state",
 											get_namespace_name(get_rel_namespace(relid)),
@@ -1079,7 +1080,7 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
 
 					sub_remove_rels = lappend(sub_remove_rels, rel);
 
-					logicalrep_worker_stop(sub->oid, relid);
+					logicalrep_worker_stop(sub->oid, relid, WORKERTYPE_TABLESYNC);
 
 					/*
 					 * For READY state, we would have already dropped the
@@ -2012,7 +2013,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	{
 		LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc);
 
-		logicalrep_worker_stop(w->subid, w->relid);
+		logicalrep_worker_stop(w->subid, w->relid, w->type);
 	}
 	list_free(subworkers);
 
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 1ad65c237c3..142a02eb5e9 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -131,7 +131,10 @@ static const struct
 		"ParallelApplyWorkerMain", ParallelApplyWorkerMain
 	},
 	{
-		"TablesyncWorkerMain", TablesyncWorkerMain
+		"TableSyncWorkerMain", TableSyncWorkerMain
+	},
+	{
+		"SequenceSyncWorkerMain", SequenceSyncWorkerMain
 	}
 };
 
diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile
index c62c8c67521..c719af1f8a9 100644
--- a/src/backend/replication/logical/Makefile
+++ b/src/backend/replication/logical/Makefile
@@ -26,6 +26,7 @@ OBJS = \
 	proto.o \
 	relation.o \
 	reorderbuffer.o \
+	sequencesync.o \
 	slotsync.o \
 	snapbuild.o \
 	syncutils.o \
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 37377f7eb63..60fb14861ab 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -243,19 +243,18 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker,
 
 /*
  * Walks the workers array and searches for one that matches given
- * subscription id and relid.
- *
- * We are only interested in the leader apply worker or table sync worker.
+ * subscription id, relid and type.
  */
 LogicalRepWorker *
-logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
+logicalrep_worker_find(Oid subid, Oid relid, LogicalRepWorkerType wtype,
+					   bool only_running)
 {
 	int			i;
 	LogicalRepWorker *res = NULL;
 
 	Assert(LWLockHeldByMe(LogicalRepWorkerLock));
 
-	/* Search for attached worker for a given subscription id. */
+	/* Search for the attached worker matching the specified criteria. */
 	for (i = 0; i < max_logical_replication_workers; i++)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
@@ -265,7 +264,7 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running)
 			continue;
 
 		if (w->in_use && w->subid == subid && w->relid == relid &&
-			(!only_running || w->proc))
+			w->type == wtype && (!only_running || w->proc))
 		{
 			res = w;
 			break;
@@ -326,6 +325,7 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype,
 	int			nparallelapplyworkers;
 	TimestampTz now;
 	bool		is_tablesync_worker = (wtype == WORKERTYPE_TABLESYNC);
+	bool		is_sequencesync_worker = (wtype == WORKERTYPE_SEQUENCESYNC);
 	bool		is_parallel_apply_worker = (wtype == WORKERTYPE_PARALLEL_APPLY);
 
 	/*----------
@@ -414,7 +414,8 @@ retry:
 	 * sync worker limit per subscription. So, just return silently as we
 	 * might get here because of an otherwise harmless race condition.
 	 */
-	if (is_tablesync_worker && nsyncworkers >= max_sync_workers_per_subscription)
+	if ((is_tablesync_worker || is_sequencesync_worker) &&
+		nsyncworkers >= max_sync_workers_per_subscription)
 	{
 		LWLockRelease(LogicalRepWorkerLock);
 		return false;
@@ -503,8 +504,16 @@ retry:
 			memcpy(bgw.bgw_extra, &subworker_dsm, sizeof(dsm_handle));
 			break;
 
+		case WORKERTYPE_SEQUENCESYNC:
+			snprintf(bgw.bgw_function_name, BGW_MAXLEN, "SequenceSyncWorkerMain");
+			snprintf(bgw.bgw_name, BGW_MAXLEN,
+					 "logical replication sequencesync worker for subscription %u",
+					 subid);
+			snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication sequencesync worker");
+			break;
+
 		case WORKERTYPE_TABLESYNC:
-			snprintf(bgw.bgw_function_name, BGW_MAXLEN, "TablesyncWorkerMain");
+			snprintf(bgw.bgw_function_name, BGW_MAXLEN, "TableSyncWorkerMain");
 			snprintf(bgw.bgw_name, BGW_MAXLEN,
 					 "logical replication tablesync worker for subscription %u sync %u",
 					 subid,
@@ -627,13 +636,13 @@ logicalrep_worker_stop_internal(LogicalRepWorker *worker, int signo)
  * Stop the logical replication worker for subid/relid, if any.
  */
 void
-logicalrep_worker_stop(Oid subid, Oid relid)
+logicalrep_worker_stop(Oid subid, Oid relid, LogicalRepWorkerType wtype)
 {
 	LogicalRepWorker *worker;
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, false);
+	worker = logicalrep_worker_find(subid, relid, wtype, false);
 
 	if (worker)
 	{
@@ -700,7 +709,7 @@ logicalrep_worker_wakeup(Oid subid, Oid relid)
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	worker = logicalrep_worker_find(subid, relid, true);
+	worker = logicalrep_worker_find(subid, relid, WORKERTYPE_APPLY, true);
 
 	if (worker)
 		logicalrep_worker_wakeup_ptr(worker);
@@ -832,6 +841,25 @@ logicalrep_launcher_onexit(int code, Datum arg)
 	LogicalRepCtx->launcher_pid = 0;
 }
 
+/*
+ * Reset the last_seqsync_start_time of the sequencesync worker in the
+ * subscription's apply worker.
+ */
+void
+logicalrep_reset_seqsync_start_time(void)
+{
+	LogicalRepWorker *worker;
+
+	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+
+	worker = logicalrep_worker_find(MyLogicalRepWorker->subid, InvalidOid,
+									WORKERTYPE_APPLY, true);
+	if (worker)
+		worker->last_seqsync_start_time = 0;
+
+	LWLockRelease(LogicalRepWorkerLock);
+}
+
 /*
  * Cleanup function.
  *
@@ -880,7 +908,7 @@ logicalrep_sync_worker_count(Oid subid)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
-		if (isTablesyncWorker(w) && w->subid == subid)
+		if (w->subid == subid && (isTableSyncWorker(w) || isSequenceSyncWorker(w)))
 			res++;
 	}
 
@@ -1246,7 +1274,7 @@ ApplyLauncherMain(Datum main_arg)
 				continue;
 
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
-			w = logicalrep_worker_find(sub->oid, InvalidOid, false);
+			w = logicalrep_worker_find(sub->oid, InvalidOid, WORKERTYPE_APPLY, false);
 			LWLockRelease(LogicalRepWorkerLock);
 
 			if (w != NULL)
@@ -1547,7 +1575,7 @@ pg_stat_get_subscription(PG_FUNCTION_ARGS)
 		worker_pid = worker.proc->pid;
 
 		values[0] = ObjectIdGetDatum(worker.subid);
-		if (isTablesyncWorker(&worker))
+		if (isTableSyncWorker(&worker))
 			values[1] = ObjectIdGetDatum(worker.relid);
 		else
 			nulls[1] = true;
@@ -1587,6 +1615,9 @@ pg_stat_get_subscription(PG_FUNCTION_ARGS)
 			case WORKERTYPE_PARALLEL_APPLY:
 				values[9] = CStringGetTextDatum("parallel apply");
 				break;
+			case WORKERTYPE_SEQUENCESYNC:
+				values[9] = CStringGetTextDatum("sequence synchronization");
+				break;
 			case WORKERTYPE_TABLESYNC:
 				values[9] = CStringGetTextDatum("table synchronization");
 				break;
diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build
index 9283e996ef4..a2268d8361e 100644
--- a/src/backend/replication/logical/meson.build
+++ b/src/backend/replication/logical/meson.build
@@ -12,6 +12,7 @@ backend_sources += files(
   'proto.c',
   'relation.c',
   'reorderbuffer.c',
+  'sequencesync.c',
   'slotsync.c',
   'snapbuild.c',
   'syncutils.c',
diff --git a/src/backend/replication/logical/sequencesync.c b/src/backend/replication/logical/sequencesync.c
new file mode 100644
index 00000000000..7ee9e2da88c
--- /dev/null
+++ b/src/backend/replication/logical/sequencesync.c
@@ -0,0 +1,630 @@
+/*-------------------------------------------------------------------------
+ * sequencesync.c
+ *	  PostgreSQL logical replication: sequence synchronization
+ *
+ * Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/replication/logical/sequencesync.c
+ *
+ * NOTES
+ *	  This file contains code for sequence synchronization for
+ *	  logical replication.
+ *
+ * Sequences to be synchronized by the sequencesync worker will
+ * be added to pg_subscription_rel in INIT state when one of the following
+ * commands is executed:
+ * CREATE SUBSCRIPTION
+ * ALTER SUBSCRIPTION ... REFRESH PUBLICATION
+ * ALTER SUBSCRIPTION ... REFRESH PUBLICATION SEQUENCES
+ *
+ * The apply worker will periodically check if there are any sequences in INIT
+ * state and will start a sequencesync worker if needed.
+ *
+ * The sequencesync worker retrieves the sequences to be synchronized from the
+ * pg_subscription_rel catalog table.  It synchronizes multiple sequences per
+ * single transaction by fetching the sequence value and page LSN from the
+ * remote publisher and updating them in the local subscriber sequence.  After
+ * synchronization, it sets the sequence state to READY.
+ *
+ * So the state progression is always just: INIT -> READY.
+ *
+ * To avoid creating too many transactions, up to MAX_SEQUENCES_SYNC_PER_BATCH
+ * (100) sequences are synchronized per transaction. The locks on the sequence
+ * relation will be periodically released at each transaction commit.
+ *
+ * XXX: An alternative design was considered where the launcher process would
+ * periodically check for sequences that need syncing and then start the
+ * sequencesync worker. However, the approach of having the apply worker
+ * manage the sequencesync worker was chosen for the following reasons:
+ * a) It avoids overloading the launcher, which handles various other
+ *    subscription requests.
+ * b) It offers a more straightforward path for extending support for
+ *    incremental sequence synchronization.
+ * c) It utilizes the existing tablesync worker code to start the sequencesync
+ *    process, thus preventing code duplication in the launcher.
+ * d) It simplifies code maintenance by consolidating changes to a single
+ *    location rather than multiple components.
+ * e) The apply worker can access the sequences that need to be synchronized
+ *    from the pg_subscription_rel system catalog. Whereas the launcher process
+ *    operates without direct database access so would need a framework to
+ *    establish connections with the databases to retrieve the sequences for
+ *    synchronization.
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/table.h"
+#include "catalog/pg_sequence.h"
+#include "catalog/pg_subscription_rel.h"
+#include "commands/sequence.h"
+#include "pgstat.h"
+#include "replication/logicallauncher.h"
+#include "replication/logicalworker.h"
+#include "replication/worker_internal.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/catcache.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/pg_lsn.h"
+#include "utils/rls.h"
+#include "utils/syscache.h"
+#include "utils/usercontext.h"
+
+/*
+ * Handle sequence synchronization cooperation from the apply worker.
+ *
+ * Start a sequencesync worker if one is not already running. The active
+ * sequencesync worker will handle all pending sequence synchronization. If any
+ * sequences remain unsynchronized after it exits, a new worker can be started
+ * in the next iteration.
+ */
+void
+ProcessSyncingSequencesForApply(void)
+{
+	LogicalRepWorker *sequencesync_worker;
+	int			nsyncworkers;
+
+	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+
+	/* Check if there is a sequencesync worker already running? */
+	sequencesync_worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
+												 InvalidOid,
+												 WORKERTYPE_SEQUENCESYNC,
+												 true);
+	if (sequencesync_worker)
+	{
+		/* Now safe to release the LWLock */
+		LWLockRelease(LogicalRepWorkerLock);
+		return;
+	}
+
+	/*
+	 * Count running sync workers for this subscription, while we have the
+	 * lock.
+	 */
+	nsyncworkers = logicalrep_sync_worker_count(MyLogicalRepWorker->subid);
+	LWLockRelease(LogicalRepWorkerLock);
+
+	check_and_launch_sync_worker(nsyncworkers, InvalidOid,
+								 &MyLogicalRepWorker->last_seqsync_start_time);
+}
+
+/*
+ * report_error_sequences
+ *
+ * Reports discrepancies in sequence data between the publisher and subscriber.
+ * It reports sequences that are missing on the publisher, as well as sequences
+ * that exist on both sides but have mismatched values.
+ */
+static void
+report_error_sequences(StringInfo missing_seqs, StringInfo mismatched_seqs)
+{
+	StringInfo	combined_error_detail = makeStringInfo();
+	StringInfo	combined_error_hint = makeStringInfo();
+
+	if (missing_seqs->len)
+	{
+		appendStringInfo(combined_error_detail, "Missing sequence(s) on publisher: (%s).",
+						 missing_seqs->data);
+		appendStringInfoString(combined_error_hint, "For missing sequences, use ALTER SUBSCRIPTION with either REFRESH PUBLICATION or REFRESH PUBLICATION SEQUENCES.");
+	}
+
+	if (mismatched_seqs->len)
+	{
+		if (missing_seqs->len)
+		{
+			appendStringInfo(combined_error_detail, "; mismatched sequence(s) on subscriber: (%s).",
+							 mismatched_seqs->data);
+			appendStringInfoString(combined_error_hint, " For mismatched sequences, alter or re-create local sequences to have matching parameters as publishers.");
+		}
+		else
+		{
+			appendStringInfo(combined_error_detail, "Mismatched sequence(s) on subscriber: (%s).",
+							 mismatched_seqs->data);
+			appendStringInfoString(combined_error_hint, "For mismatched sequences, alter or re-create local sequences to have matching parameters as publishers");
+		}
+	}
+
+	ereport(ERROR,
+			errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("logical replication sequence synchronization failed for subscription \"%s\"", MySubscription->name),
+			errdetail("%s", combined_error_detail->data),
+			errhint("%s", combined_error_hint->data));
+}
+
+/*
+ * sequence_comparator
+ *
+ * Comparator function for sorting LogicalRepSequenceInfo objects in a list.
+ * It compares sequences first by namespace name and then by sequence name.
+ */
+static int
+sequence_comparator(const ListCell *s1, const ListCell *s2)
+{
+	int			cmp;
+	LogicalRepSequenceInfo *seqinfo1 = (LogicalRepSequenceInfo *) (s1->ptr_value);
+	LogicalRepSequenceInfo *seqinfo2 = (LogicalRepSequenceInfo *) (s2->ptr_value);
+
+	/* Compare by namespace name first */
+	cmp = strcmp(seqinfo1->nspname, seqinfo2->nspname);
+	if (cmp != 0)
+		return cmp;
+
+	/* If namespace names are equal, compare by sequence name */
+	return strcmp(seqinfo1->seqname, seqinfo2->seqname);
+}
+
+/*
+ * Copy existing data of sequences from the publisher.
+ *
+ * Fetch the sequence value from the publisher and set the subscriber sequence
+ * with the same value. Caller is responsible for locking the local relation.
+ */
+static void
+copy_sequences(WalReceiverConn *conn, List *sequences_to_copy, Oid subid)
+{
+	int			total_seqs = list_length(sequences_to_copy);
+	int			current_index = 0;
+	int			search_pos = 0;
+	StringInfo	mismatched_seqs = makeStringInfo();
+	StringInfo	missing_seqs = makeStringInfo();
+	bool		run_as_owner = MySubscription->runasowner;
+
+	ereport(LOG,
+			errmsg("logical replication sequence synchronization for subscription \"%s\" - total unsynchronized: %d",
+				   MySubscription->name, total_seqs));
+
+	/* Sort the list of sequences to optimize the search */
+	list_sort(sequences_to_copy, sequence_comparator);
+
+	/*
+	 * We batch synchronize multiple sequences per transaction, because the
+	 * alternative of synchronizing each sequence individually incurs overhead
+	 * of starting and committing transactions repeatedly. On the other hand,
+	 * we want to avoid keeping this batch transaction open for extended
+	 * periods so it is currently limited to 100 sequences per batch.
+	 */
+#define MAX_SEQUENCES_SYNC_PER_BATCH 100
+
+	while (current_index < total_seqs)
+	{
+#define REMOTE_SEQ_COL_COUNT 12
+		Oid			seqRow[REMOTE_SEQ_COL_COUNT] = {TEXTOID, TEXTOID, INT8OID,
+		BOOLOID, INT8OID, LSNOID, OIDOID, INT8OID, INT8OID, INT8OID, INT8OID, BOOLOID};
+
+		int			batch_size = Min(MAX_SEQUENCES_SYNC_PER_BATCH, total_seqs - current_index);
+		int			batch_succeeded_count = 0;
+		int			batch_mismatched_count = 0;
+		int			batch_skipped_count = 0;
+		StringInfo	seqstr = makeStringInfo();
+		StringInfo	cmd = makeStringInfo();
+		WalRcvExecResult *res;
+		TupleTableSlot *slot;
+
+		StartTransactionCommand();
+
+		/*
+		 * Prepare the string of current batch sequences to fetch from the
+		 * publisher.
+		 */
+		for (int i = 0; i < batch_size; i++)
+		{
+			LogicalRepSequenceInfo *seqinfo = lfirst(list_nth_cell(sequences_to_copy, current_index + i));
+
+			if (seqstr->len > 0)
+				appendStringInfoString(seqstr, ", ");
+
+			appendStringInfo(seqstr, "(\'%s\', \'%s\')", seqinfo->nspname,
+							 seqinfo->seqname);
+		}
+
+		initStringInfo(cmd);
+		appendStringInfo(cmd,
+						 "SELECT s.schname, s.seqname, ps.*, seq.seqtypid,\n"
+						 "       seq.seqstart, seq.seqincrement, seq.seqmin,\n"
+						 "       seq.seqmax, seq.seqcycle\n"
+						 "FROM ( VALUES %s ) AS s (schname, seqname)\n"
+						 "JOIN pg_namespace n ON n.nspname = s.schname\n"
+						 "JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = s.seqname\n"
+						 "JOIN pg_sequence seq ON seq.seqrelid = c.oid\n"
+						 "JOIN LATERAL pg_get_sequence_data(seq.seqrelid) AS ps ON true\n"
+						 "ORDER BY s.schname, s.seqname\n",
+						 seqstr->data);
+
+		res = walrcv_exec(conn, cmd->data, lengthof(seqRow), seqRow);
+		if (res->status != WALRCV_OK_TUPLES)
+			ereport(ERROR,
+					errcode(ERRCODE_CONNECTION_FAILURE),
+					errmsg("could not receive list of sequence information from the publisher: %s",
+						   res->err));
+
+		destroyStringInfo(seqstr);
+
+		slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+		while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+		{
+			int			col = 0;
+			bool		isnull;
+			char	   *nspname;
+			char	   *seqname;
+			int64		last_value;
+			bool		is_called;
+			int64		log_cnt;
+			XLogRecPtr	page_lsn;
+			Oid			seqtypid;
+			int64		seqstart;
+			int64		seqmin;
+			int64		seqmax;
+			int64		seqincrement;
+			bool		seqcycle;
+			HeapTuple	tup;
+			Relation	sequence_rel;
+			Form_pg_sequence seqform;
+			LogicalRepSequenceInfo *seqinfo = NULL;
+
+			CHECK_FOR_INTERRUPTS();
+
+			nspname = TextDatumGetCString(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			seqname = TextDatumGetCString(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			last_value = DatumGetInt64(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			is_called = DatumGetBool(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			log_cnt = DatumGetInt64(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			page_lsn = DatumGetLSN(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			seqtypid = DatumGetObjectId(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			seqstart = DatumGetInt64(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			seqincrement = DatumGetInt64(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			seqmin = DatumGetInt64(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			seqmax = DatumGetInt64(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			seqcycle = DatumGetBool(slot_getattr(slot, ++col, &isnull));
+			Assert(!isnull);
+
+			/* Sanity check */
+			Assert(col == REMOTE_SEQ_COL_COUNT);
+
+			/* Retrieve the sequence object fetched from the publisher */
+			while (search_pos < total_seqs)
+			{
+				LogicalRepSequenceInfo *candidate_seq = lfirst(list_nth_cell(sequences_to_copy, search_pos));
+
+				if (!strcmp(candidate_seq->nspname, nspname) &&
+					!strcmp(candidate_seq->seqname, seqname))
+				{
+					seqinfo = candidate_seq;
+					search_pos++;
+					break;
+				}
+
+				search_pos++;
+			}
+
+			Assert(seqinfo);
+
+			seqinfo->remote_seq_fetched = true;
+
+			/* Get the local sequence */
+			sequence_rel = try_table_open(seqinfo->localrelid, RowExclusiveLock);
+			tup = SearchSysCache1(SEQRELID, ObjectIdGetDatum(seqinfo->localrelid));
+			if (!HeapTupleIsValid(tup))
+			{
+				elog(LOG, "skip synchronization of sequence \"%s.%s\" because it has been dropped concurrently",
+					 seqinfo->nspname, seqinfo->seqname);
+
+				batch_skipped_count++;
+				continue;
+			}
+
+			seqform = (Form_pg_sequence) GETSTRUCT(tup);
+
+			/* Update the sequence only if the parameters are identical */
+			if (seqform->seqtypid == seqtypid &&
+				seqform->seqmin == seqmin && seqform->seqmax == seqmax &&
+				seqform->seqcycle == seqcycle &&
+				seqform->seqstart == seqstart &&
+				seqform->seqincrement == seqincrement)
+			{
+				UserContext ucxt;
+
+				/*
+				 * Make sure that the copy command runs as the sequence owner,
+				 * unless the user has opted out of that behaviour.
+				 */
+				if (!run_as_owner)
+					SwitchToUntrustedUser(seqinfo->seqowner, &ucxt);
+
+				SetSequence(seqinfo->localrelid, last_value, log_cnt, is_called);
+
+				if (!run_as_owner)
+					RestoreUserContext(&ucxt);
+
+				UpdateSubscriptionRelState(subid, seqinfo->localrelid,
+										   SUBREL_STATE_READY, page_lsn, false);
+				ereport(DEBUG1,
+						errmsg_internal("logical replication synchronization for subscription \"%s\", sequence \"%s.%s\" has finished",
+										MySubscription->name,
+										seqinfo->nspname,
+										seqinfo->seqname));
+
+				batch_succeeded_count++;
+			}
+			else
+			{
+				if (mismatched_seqs->len)
+					appendStringInfoString(mismatched_seqs, ", ");
+
+				appendStringInfo(mismatched_seqs, "\"%s.%s\"",
+								 seqinfo->nspname, seqinfo->seqname);
+				batch_mismatched_count++;
+			}
+
+			ReleaseSysCache(tup);
+			table_close(sequence_rel, NoLock);
+		}
+
+		ereport(LOG,
+				errmsg("logical replication sequence synchronization for subscription \"%s\" - batch #%d = %d attempted, %d succeeded, %d skipped, %d mismatched, %d missing",
+					   MySubscription->name, (current_index / MAX_SEQUENCES_SYNC_PER_BATCH) + 1, batch_size,
+					   batch_succeeded_count, batch_skipped_count, batch_mismatched_count,
+					   batch_size - (batch_succeeded_count + batch_skipped_count + batch_mismatched_count)));
+
+		ExecDropSingleTupleTableSlot(slot);
+
+		walrcv_clear_result(res);
+
+		destroyStringInfo(cmd);
+
+		/* Commit this batch, and prepare for next batch */
+		CommitTransactionCommand();
+
+		/*
+		 * Sequence synchronization for this batch was incomplete because some
+		 * sequences are missing on the publisher. Identify the missing
+		 * sequences.
+		 */
+		if ((batch_succeeded_count + batch_skipped_count + batch_mismatched_count) < batch_size)
+		{
+			for (int i = 0; i < batch_size; i++)
+			{
+				LogicalRepSequenceInfo *seqinfo = lfirst(list_nth_cell(sequences_to_copy, current_index + i));
+
+				if (!seqinfo->remote_seq_fetched)
+				{
+					if (missing_seqs->len)
+						appendStringInfoString(missing_seqs, ", ");
+
+					appendStringInfo(missing_seqs, "\"%s.%s\"",
+									 seqinfo->nspname, seqinfo->seqname);
+				}
+			}
+		}
+
+		/*
+		 * current_indexes is not incremented sequentially because some
+		 * sequences may be missing, and the number of fetched rows may not
+		 * match the batch size.
+		 */
+		current_index += batch_size;
+	}
+
+	/*
+	 * Raise an error if any sequences are missing on the remote server, or if
+	 * the local and remote sequence parameters do not match.
+	 */
+	if (missing_seqs->len || mismatched_seqs->len)
+		report_error_sequences(missing_seqs, mismatched_seqs);
+
+	destroyStringInfo(missing_seqs);
+	destroyStringInfo(mismatched_seqs);
+}
+
+/*
+ * Start syncing the sequences in the sequencesync worker.
+ */
+static void
+LogicalRepSyncSequences(void)
+{
+	char	   *err;
+	bool		must_use_password;
+	Relation	rel;
+	HeapTuple	tup;
+	ScanKeyData skey[2];
+	SysScanDesc scan;
+	Oid			subid = MyLogicalRepWorker->subid;
+	List	   *sequences_to_copy = NIL;
+	StringInfoData app_name;
+
+	StartTransactionCommand();
+
+	rel = table_open(SubscriptionRelRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_subscription_rel_srsubid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(subid));
+
+	ScanKeyInit(&skey[1],
+				Anum_pg_subscription_rel_srsubstate,
+				BTEqualStrategyNumber, F_CHAREQ,
+				CharGetDatum(SUBREL_STATE_INIT));
+
+	scan = systable_beginscan(rel, InvalidOid, false,
+							  NULL, 2, skey);
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		Form_pg_subscription_rel subrel;
+		char		relkind;
+		Relation	sequence_rel;
+		LogicalRepSequenceInfo *seq_info;
+		char	   *nspname;
+		char	   *seqname;
+		MemoryContext oldctx;
+
+		CHECK_FOR_INTERRUPTS();
+
+		subrel = (Form_pg_subscription_rel) GETSTRUCT(tup);
+
+		/* Relation is either a sequence or a table */
+		relkind = get_rel_relkind(subrel->srrelid);
+		if (relkind != RELKIND_SEQUENCE)
+			continue;
+
+		/* Skip if sequence was dropped concurrently */
+		sequence_rel = try_table_open(subrel->srrelid, RowExclusiveLock);
+		if (!sequence_rel)
+			continue;
+
+		seqname = RelationGetRelationName(sequence_rel);
+		nspname = get_namespace_name(RelationGetNamespace(sequence_rel));
+
+		/* Allocate the tracking info in a permanent memory context. */
+		oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+
+		seq_info = (LogicalRepSequenceInfo *) palloc(sizeof(LogicalRepSequenceInfo));
+		seq_info->seqname = pstrdup(seqname);
+		seq_info->nspname = pstrdup(nspname);
+		seq_info->localrelid = subrel->srrelid;
+		seq_info->remote_seq_fetched = false;
+		seq_info->seqowner = sequence_rel->rd_rel->relowner;
+		sequences_to_copy = lappend(sequences_to_copy, seq_info);
+
+		MemoryContextSwitchTo(oldctx);
+
+		table_close(sequence_rel, RowExclusiveLock);
+	}
+
+	/* Cleanup */
+	systable_endscan(scan);
+	table_close(rel, AccessShareLock);
+
+	CommitTransactionCommand();
+
+	/* Is the use of a password mandatory? */
+	must_use_password = MySubscription->passwordrequired &&
+		!MySubscription->ownersuperuser;
+
+	initStringInfo(&app_name);
+	appendStringInfo(&app_name, "%s_%s", MySubscription->name, "sequencesync worker");
+
+	/*
+	 * Establish the connection to the publisher for sequence synchronization.
+	 */
+	LogRepWorkerWalRcvConn =
+		walrcv_connect(MySubscription->conninfo, true, true,
+					   must_use_password,
+					   app_name.data, &err);
+	if (LogRepWorkerWalRcvConn == NULL)
+		ereport(ERROR,
+				errcode(ERRCODE_CONNECTION_FAILURE),
+				errmsg("sequencesync worker for subscription \"%s\" could not connect to the publisher: %s",
+					   MySubscription->name, err));
+
+	pfree(app_name.data);
+
+	copy_sequences(LogRepWorkerWalRcvConn, sequences_to_copy, subid);
+
+	foreach_ptr(LogicalRepSequenceInfo, seq_info, sequences_to_copy)
+	{
+		pfree(seq_info->seqname);
+		pfree(seq_info->nspname);
+
+		sequences_to_copy = foreach_delete_current(sequences_to_copy, seq_info);
+	}
+
+	list_free(sequences_to_copy);
+}
+
+/*
+ * Execute the initial sync with error handling. Disable the subscription,
+ * if required.
+ *
+ * Allocate the slot name in long-lived context on return. Note that we don't
+ * handle FATAL errors which are probably because of system resource error and
+ * are not repeatable.
+ */
+static void
+start_sequence_sync()
+{
+	Assert(am_sequencesync_worker());
+
+	PG_TRY();
+	{
+		/* Call initial sync. */
+		LogicalRepSyncSequences();
+	}
+	PG_CATCH();
+	{
+		if (MySubscription->disableonerr)
+			DisableSubscriptionAndExit();
+		else
+		{
+			/*
+			 * Report the worker failed during sequence synchronization. Abort
+			 * the current transaction so that the stats message is sent in an
+			 * idle state.
+			 */
+			AbortOutOfAnyTransaction();
+			pgstat_report_subscription_error(MySubscription->oid,
+											 WORKERTYPE_SEQUENCESYNC);
+
+			PG_RE_THROW();
+		}
+	}
+	PG_END_TRY();
+}
+
+/* Logical Replication sequencesync worker entry point */
+void
+SequenceSyncWorkerMain(Datum main_arg)
+{
+	int			worker_slot = DatumGetInt32(main_arg);
+
+	SetupApplyOrSyncWorker(worker_slot);
+
+	start_sequence_sync();
+
+	FinishSyncWorker(WORKERTYPE_SEQUENCESYNC);
+}
diff --git a/src/backend/replication/logical/syncutils.c b/src/backend/replication/logical/syncutils.c
index 45b6d429558..ca5c278cd27 100644
--- a/src/backend/replication/logical/syncutils.c
+++ b/src/backend/replication/logical/syncutils.c
@@ -50,8 +50,10 @@ static SyncingRelationsState relation_states_validity = SYNC_RELATIONS_STATE_NEE
  * Exit routine for synchronization worker.
  */
 pg_noreturn void
-FinishSyncWorker(void)
+FinishSyncWorker(LogicalRepWorkerType wtype)
 {
+	Assert(wtype == WORKERTYPE_TABLESYNC || wtype == WORKERTYPE_SEQUENCESYNC);
+
 	/*
 	 * Commit any outstanding transaction. This is the usual case, unless
 	 * there was nothing to do for the table.
@@ -66,15 +68,26 @@ FinishSyncWorker(void)
 	XLogFlush(GetXLogWriteRecPtr());
 
 	StartTransactionCommand();
-	ereport(LOG,
-			(errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has finished",
-					MySubscription->name,
-					get_rel_name(MyLogicalRepWorker->relid))));
+	if (wtype == WORKERTYPE_TABLESYNC)
+		ereport(LOG,
+				errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has finished",
+					   MySubscription->name,
+					   get_rel_name(MyLogicalRepWorker->relid)));
+	else
+		ereport(LOG,
+				errmsg("logical replication sequence synchronization worker for subscription \"%s\" has finished",
+					   MySubscription->name));
 	CommitTransactionCommand();
 
 	/* Find the leader apply worker and signal it. */
 	logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid);
 
+	/*
+	 * This is a clean exit of the sequencesync worker; reset the
+	 * last_seqsync_start_time.
+	 */
+	logicalrep_reset_seqsync_start_time();
+
 	/* Stop gracefully */
 	proc_exit(0);
 }
@@ -89,7 +102,49 @@ InvalidateRelationStates(Datum arg, int cacheid, uint32 hashvalue)
 }
 
 /*
- * Process possible state change(s) of relations that are being synchronized.
+ * Attempt to launch a sync worker (sequence or table) if there is a worker
+ * available and the retry interval has elapsed.
+ *
+ * nsyncworkers: Number of currently running sync workers for the subscription.
+ * relid:  InvalidOid for sequence sync worker, actual relid for table sync
+ * worker.
+ * last_start_time: Pointer to the last start time of the worker.
+ */
+void
+check_and_launch_sync_worker(int nsyncworkers, Oid relid,
+							 TimestampTz *last_start_time)
+{
+	/* If there is a free sync worker slot, start a new sync worker */
+	if (nsyncworkers < max_sync_workers_per_subscription)
+	{
+		TimestampTz now = GetCurrentTimestamp();
+
+		if (!(*last_start_time) ||
+			TimestampDifferenceExceeds(*last_start_time, now,
+									   wal_retrieve_retry_interval))
+		{
+			/*
+			 * Set the last_start_time even if we fail to start the worker, so
+			 * that we won't retry until wal_retrieve_retry_interval has
+			 * elapsed.
+			 */
+			*last_start_time = now;
+			(void) logicalrep_worker_launch((relid == InvalidOid) ? WORKERTYPE_SEQUENCESYNC : WORKERTYPE_TABLESYNC,
+											MyLogicalRepWorker->dbid,
+											MySubscription->oid,
+											MySubscription->name,
+											MyLogicalRepWorker->userid,
+											relid,
+											DSM_HANDLE_INVALID,
+											false);
+		}
+	}
+}
+
+/*
+ * Process possible state change(s) of relations that are being synchronized
+ * and start new tablesync workers for the newly added tables. Also, start a
+ * new sequencesync worker for the newly added sequences.
  */
 void
 ProcessSyncingRelations(XLogRecPtr current_lsn)
@@ -97,6 +152,7 @@ ProcessSyncingRelations(XLogRecPtr current_lsn)
 	switch (MyLogicalRepWorker->type)
 	{
 		case WORKERTYPE_PARALLEL_APPLY:
+
 			/*
 			 * Skip for parallel apply workers because they only operate on
 			 * tables that are in a READY state. See pa_can_start() and
@@ -109,7 +165,24 @@ ProcessSyncingRelations(XLogRecPtr current_lsn)
 			break;
 
 		case WORKERTYPE_APPLY:
-			ProcessSyncingTablesForApply(current_lsn);
+			{
+				bool		has_pending_sequences = false;
+
+				/*
+				 * We need up-to-date sync state info for subscription tables
+				 * and sequences here.
+				 */
+				FetchRelationStates(&has_pending_sequences);
+				ProcessSyncingTablesForApply(current_lsn);
+				if (has_pending_sequences)
+					ProcessSyncingSequencesForApply();
+
+				break;
+			}
+
+		case WORKERTYPE_SEQUENCESYNC:
+			/* Should never happen. */
+			elog(ERROR, "Sequence synchronization worker not expected to process relations");
 			break;
 
 		case WORKERTYPE_UNKNOWN:
@@ -119,19 +192,25 @@ ProcessSyncingRelations(XLogRecPtr current_lsn)
 }
 
 /*
- * Common code to fetch the up-to-date sync state info into the static lists.
+ * Common code to fetch the up-to-date sync state info for tables and sequences.
  *
- * Returns true if subscription has 1 or more tables, else false.
+ * The pg_subscription_rel catalog is shared by tables and sequences. Changes
+ * to either sequences or tables can affect the validity of relation states, so
+ * we identify non-ready tables and non-ready sequences together to ensure
+ * consistency.
  *
- * Note: If this function started the transaction (indicated by the parameter)
- * then it is the caller's responsibility to commit it.
+ * Returns true if subscription has 1 or more tables, else false.
  */
 bool
-FetchRelationStates(bool *started_tx)
+FetchRelationStates(bool *has_pending_sequences)
 {
+	/*
+	 * has_subtables and has_subsequences is declared as static, since the
+	 * same value can be used until the system table is invalidated.
+	 */
 	static bool has_subtables = false;
-
-	*started_tx = false;
+	static bool has_subsequences_non_ready = false;
+	bool		started_tx = false;
 
 	if (relation_states_validity != SYNC_RELATIONS_STATE_VALID)
 	{
@@ -141,6 +220,7 @@ FetchRelationStates(bool *started_tx)
 		SubscriptionRelState *rstate;
 
 		relation_states_validity = SYNC_RELATIONS_STATE_REBUILD_STARTED;
+		has_subsequences_non_ready = false;
 
 		/* Clean the old lists. */
 		list_free_deep(table_states_not_ready);
@@ -149,7 +229,7 @@ FetchRelationStates(bool *started_tx)
 		if (!IsTransactionState())
 		{
 			StartTransactionCommand();
-			*started_tx = true;
+			started_tx = true;
 		}
 
 		/* Fetch tables and sequences that are in non-ready state. */
@@ -162,7 +242,11 @@ FetchRelationStates(bool *started_tx)
 		{
 			rstate = palloc(sizeof(SubscriptionRelState));
 			memcpy(rstate, lfirst(lc), sizeof(SubscriptionRelState));
-			table_states_not_ready = lappend(table_states_not_ready, rstate);
+
+			if (get_rel_relkind(rstate->relid) == RELKIND_SEQUENCE)
+				has_subsequences_non_ready = true;
+			else
+				table_states_not_ready = lappend(table_states_not_ready, rstate);
 		}
 		MemoryContextSwitchTo(oldctx);
 
@@ -187,5 +271,14 @@ FetchRelationStates(bool *started_tx)
 			relation_states_validity = SYNC_RELATIONS_STATE_VALID;
 	}
 
+	if (started_tx)
+	{
+		CommitTransactionCommand();
+		pgstat_report_stat(true);
+	}
+
+	if (has_pending_sequences)
+		*has_pending_sequences = has_subsequences_non_ready;
+
 	return has_subtables;
 }
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 3c777363243..b87413ff267 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -161,7 +161,7 @@ wait_for_table_state_change(Oid relid, char expected_state)
 		/* Check if the sync worker is still running and bail if not. */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 		worker = logicalrep_worker_find(MyLogicalRepWorker->subid, relid,
-										false);
+										WORKERTYPE_TABLESYNC, false);
 		LWLockRelease(LogicalRepWorkerLock);
 		if (!worker)
 			break;
@@ -208,7 +208,7 @@ wait_for_worker_state_change(char expected_state)
 		 */
 		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 		worker = logicalrep_worker_find(MyLogicalRepWorker->subid,
-										InvalidOid, false);
+										InvalidOid, WORKERTYPE_APPLY, false);
 		if (worker && worker->proc)
 			logicalrep_worker_wakeup_ptr(worker);
 		LWLockRelease(LogicalRepWorkerLock);
@@ -335,7 +335,7 @@ ProcessSyncingTablesForSync(XLogRecPtr current_lsn)
 		 */
 		replorigin_drop_by_name(originname, true, false);
 
-		FinishSyncWorker();
+		FinishSyncWorker(WORKERTYPE_TABLESYNC);
 	}
 	else
 		SpinLockRelease(&MyLogicalRepWorker->relmutex);
@@ -378,9 +378,6 @@ ProcessSyncingTablesForApply(XLogRecPtr current_lsn)
 
 	Assert(!IsTransactionState());
 
-	/* We need up-to-date sync state info for subscription tables here. */
-	FetchRelationStates(&started_tx);
-
 	/*
 	 * Prepare a hash table for tracking last start times of workers, to avoid
 	 * immediate restarts.  We don't need it if there are no tables that need
@@ -413,6 +410,14 @@ ProcessSyncingTablesForApply(XLogRecPtr current_lsn)
 	{
 		SubscriptionRelState *rstate = (SubscriptionRelState *) lfirst(lc);
 
+		if (!started_tx)
+		{
+			StartTransactionCommand();
+			started_tx = true;
+		}
+
+		Assert(get_rel_relkind(rstate->relid) != RELKIND_SEQUENCE);
+
 		if (rstate->state == SUBREL_STATE_SYNCDONE)
 		{
 			/*
@@ -426,11 +431,6 @@ ProcessSyncingTablesForApply(XLogRecPtr current_lsn)
 
 				rstate->state = SUBREL_STATE_READY;
 				rstate->lsn = current_lsn;
-				if (!started_tx)
-				{
-					StartTransactionCommand();
-					started_tx = true;
-				}
 
 				/*
 				 * Remove the tablesync origin tracking if exists.
@@ -477,8 +477,8 @@ ProcessSyncingTablesForApply(XLogRecPtr current_lsn)
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
 			syncworker = logicalrep_worker_find(MyLogicalRepWorker->subid,
-												rstate->relid, false);
-
+												rstate->relid,
+												WORKERTYPE_TABLESYNC, true);
 			if (syncworker)
 			{
 				/* Found one, update our copy of its state */
@@ -549,43 +549,19 @@ ProcessSyncingTablesForApply(XLogRecPtr current_lsn)
 				 */
 				int			nsyncworkers =
 					logicalrep_sync_worker_count(MyLogicalRepWorker->subid);
+				struct tablesync_start_time_mapping *hentry;
+				bool		found;
 
 				/* Now safe to release the LWLock */
 				LWLockRelease(LogicalRepWorkerLock);
 
-				/*
-				 * If there are free sync worker slot(s), start a new sync
-				 * worker for the table.
-				 */
-				if (nsyncworkers < max_sync_workers_per_subscription)
-				{
-					TimestampTz now = GetCurrentTimestamp();
-					struct tablesync_start_time_mapping *hentry;
-					bool		found;
-
-					hentry = hash_search(last_start_times, &rstate->relid,
-										 HASH_ENTER, &found);
+				hentry = hash_search(last_start_times, &rstate->relid,
+									 HASH_ENTER, &found);
+				if (!found)
+					hentry->last_start_time = 0;
 
-					if (!found ||
-						TimestampDifferenceExceeds(hentry->last_start_time, now,
-												   wal_retrieve_retry_interval))
-					{
-						/*
-						 * Set the last_start_time even if we fail to start
-						 * the worker, so that we won't retry until
-						 * wal_retrieve_retry_interval has elapsed.
-						 */
-						hentry->last_start_time = now;
-						(void) logicalrep_worker_launch(WORKERTYPE_TABLESYNC,
-														MyLogicalRepWorker->dbid,
-														MySubscription->oid,
-														MySubscription->name,
-														MyLogicalRepWorker->userid,
-														rstate->relid,
-														DSM_HANDLE_INVALID,
-														false);
-					}
-				}
+				check_and_launch_sync_worker(nsyncworkers, rstate->relid,
+											 &hentry->last_start_time);
 			}
 		}
 	}
@@ -1273,7 +1249,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 		case SUBREL_STATE_SYNCDONE:
 		case SUBREL_STATE_READY:
 		case SUBREL_STATE_UNKNOWN:
-			FinishSyncWorker(); /* doesn't return */
+			FinishSyncWorker(WORKERTYPE_TABLESYNC); /* doesn't return */
 	}
 
 	/* Calculate the name of the tablesync slot. */
@@ -1548,7 +1524,8 @@ start_table_sync(XLogRecPtr *origin_startpos, char **slotname)
 			 * idle state.
 			 */
 			AbortOutOfAnyTransaction();
-			pgstat_report_subscription_error(MySubscription->oid, false);
+			pgstat_report_subscription_error(MySubscription->oid,
+											 WORKERTYPE_TABLESYNC);
 
 			PG_RE_THROW();
 		}
@@ -1593,7 +1570,7 @@ run_tablesync_worker()
 
 /* Logical Replication Tablesync worker entry point */
 void
-TablesyncWorkerMain(Datum main_arg)
+TableSyncWorkerMain(Datum main_arg)
 {
 	int			worker_slot = DatumGetInt32(main_arg);
 
@@ -1601,7 +1578,7 @@ TablesyncWorkerMain(Datum main_arg)
 
 	run_tablesync_worker();
 
-	FinishSyncWorker();
+	FinishSyncWorker(WORKERTYPE_TABLESYNC);
 }
 
 /*
@@ -1615,23 +1592,16 @@ TablesyncWorkerMain(Datum main_arg)
 bool
 AllTablesyncsReady(void)
 {
-	bool		started_tx = false;
-	bool		has_subrels = false;
+	bool		has_tables;
 
 	/* We need up-to-date sync state info for subscription tables here. */
-	has_subrels = FetchRelationStates(&started_tx);
-
-	if (started_tx)
-	{
-		CommitTransactionCommand();
-		pgstat_report_stat(true);
-	}
+	has_tables = FetchRelationStates(NULL);
 
 	/*
 	 * Return false when there are no tables in subscription or not all tables
 	 * are in ready state; true otherwise.
 	 */
-	return has_subrels && (table_states_not_ready == NIL);
+	return has_tables && (table_states_not_ready == NIL);
 }
 
 /*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 1f233725b00..bc6b3e01d84 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -674,6 +674,11 @@ should_apply_changes_for_rel(LogicalRepRelMapEntry *rel)
 					(rel->state == SUBREL_STATE_SYNCDONE &&
 					 rel->statelsn <= remote_final_lsn));
 
+		case WORKERTYPE_SEQUENCESYNC:
+			/* Should never happen. */
+			elog(ERROR, "Sequence synchronization worker not expected to apply changes");
+			break;
+
 		case WORKERTYPE_UNKNOWN:
 			/* Should never happen. */
 			elog(ERROR, "Unknown worker type");
@@ -1214,7 +1219,10 @@ apply_handle_commit(StringInfo s)
 
 	apply_handle_commit_internal(&commit_data);
 
-	/* Process any tables that are being synchronized in parallel. */
+	/*
+	 * Process any tables that are being synchronized in parallel and any
+	 * newly added relations.
+	 */
 	ProcessSyncingRelations(commit_data.end_lsn);
 
 	pgstat_report_activity(STATE_IDLE, NULL);
@@ -1336,7 +1344,10 @@ apply_handle_prepare(StringInfo s)
 
 	in_remote_transaction = false;
 
-	/* Process any tables that are being synchronized in parallel. */
+	/*
+	 * Process any tables that are being synchronized in parallel and any
+	 * newly added relations.
+	 */
 	ProcessSyncingRelations(prepare_data.end_lsn);
 
 	/*
@@ -1392,7 +1403,10 @@ apply_handle_commit_prepared(StringInfo s)
 	store_flush_position(prepare_data.end_lsn, XactLastCommitEnd);
 	in_remote_transaction = false;
 
-	/* Process any tables that are being synchronized in parallel. */
+	/*
+	 * Process any tables that are being synchronized in parallel and any
+	 * newly added relations.
+	 */
 	ProcessSyncingRelations(prepare_data.end_lsn);
 
 	clear_subscription_skip_lsn(prepare_data.end_lsn);
@@ -1458,7 +1472,10 @@ apply_handle_rollback_prepared(StringInfo s)
 	store_flush_position(rollback_data.rollback_end_lsn, InvalidXLogRecPtr);
 	in_remote_transaction = false;
 
-	/* Process any tables that are being synchronized in parallel. */
+	/*
+	 * Process any tables that are being synchronized in parallel and any
+	 * newly added relations.
+	 */
 	ProcessSyncingRelations(rollback_data.rollback_end_lsn);
 
 	pgstat_report_activity(STATE_IDLE, NULL);
@@ -1593,7 +1610,10 @@ apply_handle_stream_prepare(StringInfo s)
 
 	pgstat_report_stat(false);
 
-	/* Process any tables that are being synchronized in parallel. */
+	/*
+	 * Process any tables that are being synchronized in parallel and any
+	 * newly added relations.
+	 */
 	ProcessSyncingRelations(prepare_data.end_lsn);
 
 	/*
@@ -2435,7 +2455,10 @@ apply_handle_stream_commit(StringInfo s)
 			break;
 	}
 
-	/* Process any tables that are being synchronized in parallel. */
+	/*
+	 * Process any tables that are being synchronized in parallel and any
+	 * newly added relations.
+	 */
 	ProcessSyncingRelations(commit_data.end_lsn);
 
 	pgstat_report_activity(STATE_IDLE, NULL);
@@ -4082,7 +4105,10 @@ LogicalRepApplyLoop(XLogRecPtr last_received)
 			AcceptInvalidationMessages();
 			maybe_reread_subscription();
 
-			/* Process any table synchronization changes. */
+			/*
+			 * Process any tables that are being synchronized in parallel and
+			 * any newly added relations.
+			 */
 			ProcessSyncingRelations(last_received);
 		}
 
@@ -5262,7 +5288,8 @@ start_apply(XLogRecPtr origin_startpos)
 			 * idle state.
 			 */
 			AbortOutOfAnyTransaction();
-			pgstat_report_subscription_error(MySubscription->oid, !am_tablesync_worker());
+			pgstat_report_subscription_error(MySubscription->oid,
+											 WORKERTYPE_APPLY);
 
 			PG_RE_THROW();
 		}
@@ -5382,8 +5409,8 @@ run_apply_worker()
 }
 
 /*
- * Common initialization for leader apply worker, parallel apply worker and
- * tablesync worker.
+ * Common initialization for leader apply worker, parallel apply worker,
+ * tablesync worker and sequencesync worker.
  *
  * Initialize the database connection, in-memory subscription and necessary
  * config options.
@@ -5486,6 +5513,10 @@ InitializeLogRepWorker(void)
 				(errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has started",
 						MySubscription->name,
 						get_rel_name(MyLogicalRepWorker->relid))));
+	else if (am_sequencesync_worker())
+		ereport(LOG,
+				(errmsg("logical replication sequence synchronization worker for subscription \"%s\" has started",
+						MySubscription->name)));
 	else
 		ereport(LOG,
 				(errmsg("logical replication apply worker for subscription \"%s\" has started",
@@ -5505,14 +5536,16 @@ replorigin_reset(int code, Datum arg)
 	replorigin_session_origin_timestamp = 0;
 }
 
-/* Common function to setup the leader apply or tablesync worker. */
+/*
+ * Common function to setup the leader apply, tablesync and sequencesync worker.
+ */
 void
 SetupApplyOrSyncWorker(int worker_slot)
 {
 	/* Attach to slot */
 	logicalrep_worker_attach(worker_slot);
 
-	Assert(am_tablesync_worker() || am_leader_apply_worker());
+	Assert(am_tablesync_worker() || am_sequencesync_worker() || am_leader_apply_worker());
 
 	/* Setup signal handling */
 	pqsignal(SIGHUP, SignalHandlerForConfigReload);
@@ -5583,6 +5616,10 @@ ApplyWorkerMain(Datum main_arg)
 void
 DisableSubscriptionAndExit(void)
 {
+	LogicalRepWorkerType wtype = am_tablesync_worker() ? WORKERTYPE_TABLESYNC :
+		(am_sequencesync_worker()) ? WORKERTYPE_SEQUENCESYNC :
+		WORKERTYPE_APPLY;
+
 	/*
 	 * Emit the error message, and recover from the error state to an idle
 	 * state
@@ -5595,9 +5632,11 @@ DisableSubscriptionAndExit(void)
 
 	RESUME_INTERRUPTS();
 
-	/* Report the worker failed during either table synchronization or apply */
-	pgstat_report_subscription_error(MyLogicalRepWorker->subid,
-									 !am_tablesync_worker());
+	/*
+	 * Report the worker failed during either sequence synchronization or
+	 * table synchronization or apply.
+	 */
+	pgstat_report_subscription_error(MyLogicalRepWorker->subid, wtype);
 
 	/* Disable the subscription */
 	StartTransactionCommand();
diff --git a/src/backend/utils/activity/pgstat_subscription.c b/src/backend/utils/activity/pgstat_subscription.c
index f9a1c831a07..002d630d4ae 100644
--- a/src/backend/utils/activity/pgstat_subscription.c
+++ b/src/backend/utils/activity/pgstat_subscription.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "replication/worker_internal.h"
 #include "utils/pgstat_internal.h"
 
 
@@ -24,7 +25,7 @@
  * Report a subscription error.
  */
 void
-pgstat_report_subscription_error(Oid subid, bool is_apply_error)
+pgstat_report_subscription_error(Oid subid, LogicalRepWorkerType wtype)
 {
 	PgStat_EntryRef *entry_ref;
 	PgStat_BackendSubEntry *pending;
@@ -33,10 +34,25 @@ pgstat_report_subscription_error(Oid subid, bool is_apply_error)
 										  InvalidOid, subid, NULL);
 	pending = entry_ref->pending;
 
-	if (is_apply_error)
-		pending->apply_error_count++;
-	else
-		pending->sync_error_count++;
+	switch (wtype)
+	{
+		case WORKERTYPE_APPLY:
+			pending->apply_error_count++;
+			break;
+
+		case WORKERTYPE_SEQUENCESYNC:
+			pending->sequence_sync_error_count++;
+			break;
+
+		case WORKERTYPE_TABLESYNC:
+			pending->sync_error_count++;
+			break;
+
+		default:
+			/* Should never happen. */
+			Assert(0);
+			break;
+	}
 }
 
 /*
@@ -115,6 +131,7 @@ pgstat_subscription_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
 
 #define SUB_ACC(fld) shsubent->stats.fld += localent->fld
 	SUB_ACC(apply_error_count);
+	SUB_ACC(sequence_sync_error_count);
 	SUB_ACC(sync_error_count);
 	for (int i = 0; i < CONFLICT_NUM_TYPES; i++)
 		SUB_ACC(conflict_count[i]);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index c756c2bebaa..b6f375cf855 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2171,7 +2171,7 @@ pg_stat_get_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_subscription_stats(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS	12
+#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS	13
 	Oid			subid = PG_GETARG_OID(0);
 	TupleDesc	tupdesc;
 	Datum		values[PG_STAT_GET_SUBSCRIPTION_STATS_COLS] = {0};
@@ -2189,25 +2189,27 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS)
 					   OIDOID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "apply_error_count",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "sync_error_count",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "sequence_sync_error_count",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 4, "confl_insert_exists",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 4, "sync_error_count",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 5, "confl_update_origin_differs",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 5, "confl_insert_exists",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 6, "confl_update_exists",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 6, "confl_update_origin_differs",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 7, "confl_update_deleted",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 7, "confl_update_exists",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 8, "confl_update_missing",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 8, "confl_update_deleted",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 9, "confl_delete_origin_differs",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 9, "confl_update_missing",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 10, "confl_delete_missing",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 10, "confl_delete_origin_differs",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 11, "confl_multiple_unique_conflicts",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 11, "confl_delete_missing",
 					   INT8OID, -1, 0);
-	TupleDescInitEntry(tupdesc, (AttrNumber) 12, "stats_reset",
+	TupleDescInitEntry(tupdesc, (AttrNumber) 12, "confl_multiple_unique_conflicts",
+					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 13, "stats_reset",
 					   TIMESTAMPTZOID, -1, 0);
 	BlessTupleDesc(tupdesc);
 
@@ -2224,6 +2226,9 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS)
 	/* apply_error_count */
 	values[i++] = Int64GetDatum(subentry->apply_error_count);
 
+	/* sequence_sync_error_count */
+	values[i++] = Int64GetDatum(subentry->sequence_sync_error_count);
+
 	/* sync_error_count */
 	values[i++] = Int64GetDatum(subentry->sync_error_count);
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index d14b1678e7f..80181825240 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3356,7 +3356,7 @@ struct config_int ConfigureNamesInt[] =
 		{"max_sync_workers_per_subscription",
 			PGC_SIGHUP,
 			REPLICATION_SUBSCRIBERS,
-			gettext_noop("Maximum number of table synchronization workers per subscription."),
+			gettext_noop("Maximum number of workers per subscription for synchronizing tables and sequences."),
 			NULL,
 		},
 		&max_sync_workers_per_subscription,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4660e42d775..dce0c2ce108 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5688,9 +5688,9 @@
 { oid => '6231', descr => 'statistics: information about subscription stats',
   proname => 'pg_stat_get_subscription_stats', provolatile => 's',
   proparallel => 'r', prorettype => 'record', proargtypes => 'oid',
-  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
-  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{subid,subid,apply_error_count,sync_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_deleted,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,confl_multiple_unique_conflicts,stats_reset}',
+  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{subid,subid,apply_error_count,sequence_sync_error_count,sync_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_deleted,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,confl_multiple_unique_conflicts,stats_reset}',
   prosrc => 'pg_stat_get_subscription_stats' },
 { oid => '6118', descr => 'statistics: information about subscription',
   proname => 'pg_stat_get_subscription', prorows => '10', proisstrict => 'f',
diff --git a/src/include/catalog/pg_subscription_rel.h b/src/include/catalog/pg_subscription_rel.h
index 87fd96e0ff5..5e0a5a989c2 100644
--- a/src/include/catalog/pg_subscription_rel.h
+++ b/src/include/catalog/pg_subscription_rel.h
@@ -82,6 +82,15 @@ typedef struct SubscriptionRelState
 	char		state;
 } SubscriptionRelState;
 
+typedef struct LogicalRepSequenceInfo
+{
+	char	   *seqname;
+	char	   *nspname;
+	Oid			localrelid;
+	bool		remote_seq_fetched;
+	Oid			seqowner;
+} LogicalRepSequenceInfo;
+
 extern void AddSubscriptionRelState(Oid subid, Oid relid, char state,
 									XLogRecPtr sublsn, bool retain_lock);
 extern void UpdateSubscriptionRelState(Oid subid, Oid relid, char state,
diff --git a/src/include/commands/sequence.h b/src/include/commands/sequence.h
index 9ac0b67683d..3aec610028f 100644
--- a/src/include/commands/sequence.h
+++ b/src/include/commands/sequence.h
@@ -45,6 +45,8 @@ typedef FormData_pg_sequence_data *Form_pg_sequence_data;
 /* XLOG stuff */
 #define XLOG_SEQ_LOG			0x00
 
+#define SEQ_LOG_CNT_INVALID		0
+
 typedef struct xl_seq_rec
 {
 	RelFileLocator locator;
@@ -60,6 +62,7 @@ extern ObjectAddress AlterSequence(ParseState *pstate, AlterSeqStmt *stmt);
 extern void SequenceChangePersistence(Oid relid, char newrelpersistence);
 extern void DeleteSequenceTuple(Oid relid);
 extern void ResetSequence(Oid seq_relid);
+extern void SetSequence(Oid relid, int64 next, int64 log_cnt, bool is_called);
 extern void ResetSequenceCaches(void);
 
 extern void seq_redo(XLogReaderState *record);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 202bd2d5ace..4bc05518c3a 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -15,6 +15,7 @@
 #include "portability/instr_time.h"
 #include "postmaster/pgarch.h"	/* for MAX_XFN_CHARS */
 #include "replication/conflict.h"
+#include "replication/worker_internal.h"
 #include "utils/backend_progress.h" /* for backward compatibility */	/* IWYU pragma: export */
 #include "utils/backend_status.h"	/* for backward compatibility */	/* IWYU pragma: export */
 #include "utils/pgstat_kind.h"
@@ -107,6 +108,7 @@ typedef struct PgStat_FunctionCallUsage
 typedef struct PgStat_BackendSubEntry
 {
 	PgStat_Counter apply_error_count;
+	PgStat_Counter sequence_sync_error_count;
 	PgStat_Counter sync_error_count;
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 } PgStat_BackendSubEntry;
@@ -413,6 +415,7 @@ typedef struct PgStat_SLRUStats
 typedef struct PgStat_StatSubEntry
 {
 	PgStat_Counter apply_error_count;
+	PgStat_Counter sequence_sync_error_count;
 	PgStat_Counter sync_error_count;
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
 	TimestampTz stat_reset_timestamp;
@@ -763,7 +766,8 @@ extern PgStat_SLRUStats *pgstat_fetch_slru(void);
  * Functions in pgstat_subscription.c
  */
 
-extern void pgstat_report_subscription_error(Oid subid, bool is_apply_error);
+extern void pgstat_report_subscription_error(Oid subid,
+											 LogicalRepWorkerType wtype);
 extern void pgstat_report_subscription_conflict(Oid subid, ConflictType type);
 extern void pgstat_create_subscription(Oid subid);
 extern void pgstat_drop_subscription(Oid subid);
diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h
index 88912606e4d..56fa79b648e 100644
--- a/src/include/replication/logicalworker.h
+++ b/src/include/replication/logicalworker.h
@@ -18,7 +18,8 @@ extern PGDLLIMPORT volatile sig_atomic_t ParallelApplyMessagePending;
 
 extern void ApplyWorkerMain(Datum main_arg);
 extern void ParallelApplyWorkerMain(Datum main_arg);
-extern void TablesyncWorkerMain(Datum main_arg);
+extern void TableSyncWorkerMain(Datum main_arg);
+extern void SequenceSyncWorkerMain(Datum main_arg);
 
 extern bool IsLogicalWorker(void);
 extern bool IsLogicalParallelApplyWorker(void);
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 7920908395d..85e31e76cbe 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -30,6 +30,7 @@ typedef enum LogicalRepWorkerType
 {
 	WORKERTYPE_UNKNOWN = 0,
 	WORKERTYPE_TABLESYNC,
+	WORKERTYPE_SEQUENCESYNC,
 	WORKERTYPE_APPLY,
 	WORKERTYPE_PARALLEL_APPLY,
 } LogicalRepWorkerType;
@@ -103,6 +104,8 @@ typedef struct LogicalRepWorker
 	TimestampTz last_recv_time;
 	XLogRecPtr	reply_lsn;
 	TimestampTz reply_time;
+
+	TimestampTz last_seqsync_start_time;
 } LogicalRepWorker;
 
 /*
@@ -252,6 +255,7 @@ extern PGDLLIMPORT List *table_states_not_ready;
 
 extern void logicalrep_worker_attach(int slot);
 extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid,
+												LogicalRepWorkerType wtype,
 												bool only_running);
 extern List *logicalrep_workers_find(Oid subid, bool only_running,
 									 bool acquire_lock);
@@ -260,12 +264,16 @@ extern bool logicalrep_worker_launch(LogicalRepWorkerType wtype,
 									 Oid userid, Oid relid,
 									 dsm_handle subworker_dsm,
 									 bool retain_dead_tuples);
-extern void logicalrep_worker_stop(Oid subid, Oid relid);
+extern void check_and_launch_sync_worker(int nsyncworkers, Oid relid,
+										 TimestampTz *last_start_time);
+extern void logicalrep_worker_stop(Oid subid, Oid relid,
+								   LogicalRepWorkerType wtype);
 extern void logicalrep_pa_worker_stop(ParallelApplyWorkerInfo *winfo);
 extern void logicalrep_worker_wakeup(Oid subid, Oid relid);
 extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker);
 
 extern int	logicalrep_sync_worker_count(Oid subid);
+extern void logicalrep_reset_seqsync_start_time(void);
 
 extern void ReplicationOriginNameForLogicalRep(Oid suboid, Oid relid,
 											   char *originname, Size szoriginname);
@@ -275,11 +283,12 @@ extern void UpdateTwoPhaseState(Oid suboid, char new_state);
 
 extern void ProcessSyncingTablesForSync(XLogRecPtr current_lsn);
 extern void ProcessSyncingTablesForApply(XLogRecPtr current_lsn);
+extern void ProcessSyncingSequencesForApply(void);
 
-pg_noreturn extern void FinishSyncWorker(void);
+pg_noreturn extern void FinishSyncWorker(LogicalRepWorkerType wtype);
 extern void InvalidateRelationStates(Datum arg, int cacheid, uint32 hashvalue);
 extern void ProcessSyncingRelations(XLogRecPtr current_lsn);
-extern bool FetchRelationStates(bool *started_tx);
+extern bool FetchRelationStates(bool *has_pending_sequences);
 
 extern void stream_start_internal(TransactionId xid, bool first_segment);
 extern void stream_stop_internal(TransactionId xid);
@@ -344,15 +353,25 @@ extern void pa_decr_and_wait_stream_block(void);
 extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo,
 						   XLogRecPtr remote_lsn);
 
+#define isApplyWorker(worker) ((worker)->in_use && \
+							   (worker)->type == WORKERTYPE_APPLY)
 #define isParallelApplyWorker(worker) ((worker)->in_use && \
 									   (worker)->type == WORKERTYPE_PARALLEL_APPLY)
-#define isTablesyncWorker(worker) ((worker)->in_use && \
+#define isTableSyncWorker(worker) ((worker)->in_use && \
 								   (worker)->type == WORKERTYPE_TABLESYNC)
+#define isSequenceSyncWorker(worker) ((worker)->in_use && \
+									  (worker)->type == WORKERTYPE_SEQUENCESYNC)
 
 static inline bool
 am_tablesync_worker(void)
 {
-	return isTablesyncWorker(MyLogicalRepWorker);
+	return isTableSyncWorker(MyLogicalRepWorker);
+}
+
+static inline bool
+am_sequencesync_worker(void)
+{
+	return isSequenceSyncWorker(MyLogicalRepWorker);
 }
 
 static inline bool
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4e2d6b693c6..3a73413738e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2188,6 +2188,7 @@ pg_stat_subscription| SELECT su.oid AS subid,
 pg_stat_subscription_stats| SELECT ss.subid,
     s.subname,
     ss.apply_error_count,
+    ss.sequence_sync_error_count,
     ss.sync_error_count,
     ss.confl_insert_exists,
     ss.confl_update_origin_differs,
@@ -2199,7 +2200,7 @@ pg_stat_subscription_stats| SELECT ss.subid,
     ss.confl_multiple_unique_conflicts,
     ss.stats_reset
    FROM pg_subscription s,
-    LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sync_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_deleted, confl_update_missing, confl_delete_origin_differs, confl_delete_missing, confl_multiple_unique_conflicts, stats_reset);
+    LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sequence_sync_error_count, sync_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_deleted, confl_update_missing, confl_delete_origin_differs, confl_delete_missing, confl_multiple_unique_conflicts, stats_reset);
 pg_stat_sys_indexes| SELECT relid,
     indexrelid,
     schemaname,
diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build
index 586ffba434e..a6c267a8a2c 100644
--- a/src/test/subscription/meson.build
+++ b/src/test/subscription/meson.build
@@ -42,6 +42,7 @@ tests += {
       't/033_run_as_table_owner.pl',
       't/034_temporal.pl',
       't/035_conflicts.pl',
+      't/036_sequences.pl',
       't/100_bugs.pl',
     ],
   },
diff --git a/src/test/subscription/t/026_stats.pl b/src/test/subscription/t/026_stats.pl
index 00a1c2fcd48..3c0b1db0510 100644
--- a/src/test/subscription/t/026_stats.pl
+++ b/src/test/subscription/t/026_stats.pl
@@ -21,7 +21,8 @@ $node_subscriber->start;
 
 sub create_sub_pub_w_errors
 {
-	my ($node_publisher, $node_subscriber, $db, $table_name) = @_;
+	my ($node_publisher, $node_subscriber, $db, $table_name, $sequence_name)
+	  = @_;
 	# Initial table setup on both publisher and subscriber. On subscriber we
 	# create the same tables but with primary keys. Also, insert some data that
 	# will conflict with the data replicated from publisher later.
@@ -32,6 +33,7 @@ sub create_sub_pub_w_errors
 	CREATE TABLE $table_name(a int);
 	ALTER TABLE $table_name REPLICA IDENTITY FULL;
 	INSERT INTO $table_name VALUES (1);
+	CREATE SEQUENCE $sequence_name;
 	COMMIT;
 	]);
 	$node_subscriber->safe_psql(
@@ -40,45 +42,56 @@ sub create_sub_pub_w_errors
 	BEGIN;
 	CREATE TABLE $table_name(a int primary key);
 	INSERT INTO $table_name VALUES (1);
+	CREATE SEQUENCE $sequence_name INCREMENT BY 10;
 	COMMIT;
 	]);
 
 	# Set up publication.
 	my $pub_name = $table_name . '_pub';
+	my $pub_seq_name = $sequence_name . '_pub';
 	my $publisher_connstr = $node_publisher->connstr . qq( dbname=$db);
 
-	$node_publisher->safe_psql($db,
-		qq(CREATE PUBLICATION $pub_name FOR TABLE $table_name));
+	$node_publisher->safe_psql(
+		$db,
+		qq[
+	CREATE PUBLICATION $pub_name FOR TABLE $table_name;
+	CREATE PUBLICATION $pub_seq_name FOR ALL SEQUENCES;
+	]);
 
 	# Create subscription. The tablesync for table on subscription will enter into
-	# infinite error loop due to violating the unique constraint.
+	# infinite error loop due to violating the unique constraint. The sequencesync
+	# will also fail due to different sequence increment values on publisher and
+	# subscriber.
 	my $sub_name = $table_name . '_sub';
 	$node_subscriber->safe_psql($db,
-		qq(CREATE SUBSCRIPTION $sub_name CONNECTION '$publisher_connstr' PUBLICATION $pub_name)
+		qq(CREATE SUBSCRIPTION $sub_name CONNECTION '$publisher_connstr' PUBLICATION $pub_name, $pub_seq_name)
 	);
 
 	$node_publisher->wait_for_catchup($sub_name);
 
-	# Wait for the tablesync error to be reported.
+	# Wait for the tablesync and sequencesync error to be reported.
 	$node_subscriber->poll_query_until(
 		$db,
 		qq[
-	SELECT sync_error_count > 0
-	FROM pg_stat_subscription_stats
-	WHERE subname = '$sub_name'
+	SELECT count(1) = 1 FROM pg_stat_subscription_stats
+	WHERE subname = '$sub_name' and sync_error_count > 0 and sequence_sync_error_count > 0
 	])
 	  or die
 	  qq(Timed out while waiting for tablesync errors for subscription '$sub_name');
 
+	# Change the sequence start value on the subscriber so that it doesn't error out.
+	$node_subscriber->safe_psql($db,
+		qq(ALTER SEQUENCE $sequence_name INCREMENT 1));
+
 	# Truncate test_tab1 so that tablesync worker can continue.
 	$node_subscriber->safe_psql($db, qq(TRUNCATE $table_name));
 
-	# Wait for initial tablesync to finish.
+	# Wait for initial sync to finish.
 	$node_subscriber->poll_query_until(
 		$db,
 		qq[
-	SELECT count(1) = 1 FROM pg_subscription_rel
-	WHERE srrelid = '$table_name'::regclass AND srsubstate in ('r', 's')
+	SELECT count(1) = 2 FROM pg_subscription_rel
+	WHERE srrelid IN ('$table_name'::regclass, '$sequence_name'::regclass) AND srsubstate in ('r', 's')
 	])
 	  or die
 	  qq(Timed out while waiting for subscriber to synchronize data for table '$table_name'.);
@@ -136,22 +149,24 @@ is($result, qq(0),
 
 # Create the publication and subscription with sync and apply errors
 my $table1_name = 'test_tab1';
+my $sequence1_name = 'test_seq1';
 my ($pub1_name, $sub1_name) =
   create_sub_pub_w_errors($node_publisher, $node_subscriber, $db,
-	$table1_name);
+	$table1_name, $sequence1_name);
 
 # Apply errors, sync errors, and conflicts are > 0 and stats_reset timestamp is NULL
 is( $node_subscriber->safe_psql(
 		$db,
 		qq(SELECT apply_error_count > 0,
 	sync_error_count > 0,
+	sequence_sync_error_count > 0,
 	confl_insert_exists > 0,
 	confl_delete_missing > 0,
 	stats_reset IS NULL
 	FROM pg_stat_subscription_stats
 	WHERE subname = '$sub1_name')
 	),
-	qq(t|t|t|t|t),
+	qq(t|t|t|t|t|t),
 	qq(Check that apply errors, sync errors, and conflicts are > 0 and stats_reset is NULL for subscription '$sub1_name'.)
 );
 
@@ -165,13 +180,14 @@ is( $node_subscriber->safe_psql(
 		$db,
 		qq(SELECT apply_error_count = 0,
 	sync_error_count = 0,
+	sequence_sync_error_count = 0,
 	confl_insert_exists = 0,
 	confl_delete_missing = 0,
 	stats_reset IS NOT NULL
 	FROM pg_stat_subscription_stats
 	WHERE subname = '$sub1_name')
 	),
-	qq(t|t|t|t|t),
+	qq(t|t|t|t|t|t),
 	qq(Confirm that apply errors, sync errors, and conflicts are 0 and stats_reset is not NULL after reset for subscription '$sub1_name'.)
 );
 
@@ -198,22 +214,24 @@ is( $node_subscriber->safe_psql(
 
 # Make second subscription and publication
 my $table2_name = 'test_tab2';
+my $sequence2_name = 'test_seq2';
 my ($pub2_name, $sub2_name) =
   create_sub_pub_w_errors($node_publisher, $node_subscriber, $db,
-	$table2_name);
+	$table2_name, $sequence2_name);
 
 # Apply errors, sync errors, and conflicts are > 0 and stats_reset timestamp is NULL
 is( $node_subscriber->safe_psql(
 		$db,
 		qq(SELECT apply_error_count > 0,
 	sync_error_count > 0,
+	sequence_sync_error_count > 0,
 	confl_insert_exists > 0,
 	confl_delete_missing > 0,
 	stats_reset IS NULL
 	FROM pg_stat_subscription_stats
 	WHERE subname = '$sub2_name')
 	),
-	qq(t|t|t|t|t),
+	qq(t|t|t|t|t|t),
 	qq(Confirm that apply errors, sync errors, and conflicts are > 0 and stats_reset is NULL for sub '$sub2_name'.)
 );
 
@@ -226,13 +244,14 @@ is( $node_subscriber->safe_psql(
 		$db,
 		qq(SELECT apply_error_count = 0,
 	sync_error_count = 0,
+	sequence_sync_error_count = 0,
 	confl_insert_exists = 0,
 	confl_delete_missing = 0,
 	stats_reset IS NOT NULL
 	FROM pg_stat_subscription_stats
 	WHERE subname = '$sub1_name')
 	),
-	qq(t|t|t|t|t),
+	qq(t|t|t|t|t|t),
 	qq(Confirm that apply errors, sync errors, and conflicts are 0 and stats_reset is not NULL for sub '$sub1_name' after reset.)
 );
 
@@ -240,13 +259,14 @@ is( $node_subscriber->safe_psql(
 		$db,
 		qq(SELECT apply_error_count = 0,
 	sync_error_count = 0,
+	sequence_sync_error_count = 0,
 	confl_insert_exists = 0,
 	confl_delete_missing = 0,
 	stats_reset IS NOT NULL
 	FROM pg_stat_subscription_stats
 	WHERE subname = '$sub2_name')
 	),
-	qq(t|t|t|t|t),
+	qq(t|t|t|t|t|t),
 	qq(Confirm that apply errors, sync errors, and conflicts are 0 and stats_reset is not NULL for sub '$sub2_name' after reset.)
 );
 
diff --git a/src/test/subscription/t/036_sequences.pl b/src/test/subscription/t/036_sequences.pl
new file mode 100644
index 00000000000..580da809114
--- /dev/null
+++ b/src/test/subscription/t/036_sequences.pl
@@ -0,0 +1,239 @@
+
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# This tests that sequences are synced correctly to the subscriber
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize publisher node
+my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+
+# Avoid checkpoint during the test, otherwise, extra values will be fetched for
+# the sequences which will cause the test to fail randomly.
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->append_conf('postgresql.conf', 'checkpoint_timeout = 1h');
+$node_publisher->start;
+
+# Initialize subscriber node
+my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_subscriber->init;
+$node_subscriber->start;
+
+# Setup structure on the publisher
+my $ddl = qq(
+	CREATE TABLE regress_seq_test (v BIGINT);
+	CREATE SEQUENCE regress_s1;
+);
+$node_publisher->safe_psql('postgres', $ddl);
+
+# Setup the same structure on the subscriber, plus some extra sequences that
+# we'll create on the publisher later
+$ddl = qq(
+	CREATE TABLE regress_seq_test (v BIGINT);
+	CREATE SEQUENCE regress_s1;
+	CREATE SEQUENCE regress_s2;
+	CREATE SEQUENCE regress_s3;
+	CREATE SEQUENCE regress_s4
+);
+$node_subscriber->safe_psql('postgres', $ddl);
+
+# Insert initial test data
+$node_publisher->safe_psql(
+	'postgres', qq(
+	-- generate a number of values using the sequence
+	INSERT INTO regress_seq_test SELECT nextval('regress_s1') FROM generate_series(1,100);
+));
+
+# Setup logical replication pub/sub
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION regress_seq_pub FOR ALL SEQUENCES");
+$node_subscriber->safe_psql('postgres',
+	"CREATE SUBSCRIPTION regress_seq_sub CONNECTION '$publisher_connstr' PUBLICATION regress_seq_pub"
+);
+
+# Wait for initial sync to finish
+my $synced_query =
+  "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check the initial data on subscriber
+my $result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s1;
+));
+is($result, '100|32|t', 'initial test data replicated');
+
+##########
+## ALTER SUBSCRIPTION ... REFRESH PUBLICATION should cause sync of new
+# sequences of the publisher, but changes to existing sequences should
+# not be synced.
+##########
+
+# Create a new sequence 'regress_s2', and update existing sequence 'regress_s1'
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SEQUENCE regress_s2;
+	INSERT INTO regress_seq_test SELECT nextval('regress_s2') FROM generate_series(1,100);
+
+    -- Existing sequence
+	INSERT INTO regress_seq_test SELECT nextval('regress_s1') FROM generate_series(1,100);
+));
+
+# Do ALTER SUBSCRIPTION ... REFRESH PUBLICATION
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	ALTER SUBSCRIPTION regress_seq_sub REFRESH PUBLICATION
+));
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+$result = $node_publisher->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s1;
+));
+is($result, '200|31|t', 'Check sequence value in the publisher');
+
+# Check - existing sequence is not synced
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s1;
+));
+is($result, '100|32|t',
+	'REFRESH PUBLICATION does not sync existing sequence');
+
+# Check - newly published sequence is synced
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s2;
+));
+is($result, '100|32|t',
+	'REFRESH PUBLICATION will sync newly published sequence');
+
+##########
+## ALTER SUBSCRIPTION ... REFRESH PUBLICATION SEQUENCES should cause sync of
+# new sequences of the publisher, and changes to existing sequences should
+# also be synced.
+##########
+
+# Create a new sequence 'regress_s3', and update the existing sequence
+# 'regress_s2'.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SEQUENCE regress_s3;
+	INSERT INTO regress_seq_test SELECT nextval('regress_s3') FROM generate_series(1,100);
+
+	-- Existing sequence
+	INSERT INTO regress_seq_test SELECT nextval('regress_s2') FROM generate_series(1,100);
+));
+
+# Do ALTER SUBSCRIPTION ... REFRESH PUBLICATION SEQUENCES
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	ALTER SUBSCRIPTION regress_seq_sub REFRESH PUBLICATION SEQUENCES
+));
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+# Check - existing sequences are synced
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s1;
+));
+is($result, '200|31|t',
+	'REFRESH PUBLICATION SEQUENCES will sync existing sequences');
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s2;
+));
+is($result, '200|31|t',
+	'REFRESH PUBLICATION SEQUENCES will sync existing sequences');
+
+# Check - newly published sequence is synced
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s3;
+));
+is($result, '100|32|t',
+	'REFRESH PUBLICATION SEQUENCES will sync newly published sequence');
+
+##########
+## ALTER SUBSCRIPTION ... REFRESH PUBLICATION with (copy_data = off) should
+# not update the sequence values for the new sequence.
+##########
+
+# Create a new sequence 'regress_s4'
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SEQUENCE regress_s4;
+	INSERT INTO regress_seq_test SELECT nextval('regress_s4') FROM generate_series(1,100);
+));
+
+# Do ALTER SUBSCRIPTION ... REFRESH PUBLICATION
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	ALTER SUBSCRIPTION regress_seq_sub REFRESH PUBLICATION with (copy_data = false);
+));
+$node_subscriber->poll_query_until('postgres', $synced_query)
+  or die "Timed out while waiting for subscriber to synchronize data";
+
+$result = $node_publisher->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s4;
+));
+is($result, '100|32|t', 'Check sequence value in the publisher');
+
+# Check - newly published sequence values are not updated
+$result = $node_subscriber->safe_psql(
+	'postgres', qq(
+	SELECT last_value, log_cnt, is_called FROM regress_s4;
+));
+is($result, '1|0|f',
+	'REFRESH PUBLICATION will not sync newly published sequence with copy_data as off'
+);
+
+##########
+# ALTER SUBSCRIPTION ... REFRESH PUBLICATION SEQUENCES should throw an error
+# for sequence definition not matching between the publisher and the subscriber.
+##########
+
+# Create a new sequence 'regress_s5' whose START value is not the same in the
+# publisher and subscriber.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE SEQUENCE regress_s5 START 1 INCREMENT 2;
+));
+
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE SEQUENCE regress_s5 START 10 INCREMENT 2;
+));
+
+my $log_offset = -s $node_subscriber->logfile;
+
+# Do ALTER SUBSCRIPTION ... REFRESH PUBLICATION SEQUENCES
+$node_subscriber->safe_psql(
+	'postgres', "
+    ALTER SUBSCRIPTION regress_seq_sub REFRESH PUBLICATION SEQUENCES"
+);
+
+# Confirm that the warning for parameters differing is logged.
+$node_subscriber->wait_for_log(
+	qr/ERROR: ( [A-Z0-9]+:)? logical replication sequence synchronization failed for subscription "regress_seq_sub"\n.*DETAIL:.* Mismatched sequence\(s\) on subscriber: \("public.regress_s5"\)/,
+	$log_offset);
+
+$node_publisher->safe_psql(
+	'postgres', qq(
+	DROP SEQUENCE regress_s5;
+));
+
+# Confirm that the warning for missing sequence is logged.
+$node_subscriber->wait_for_log(
+	qr/ERROR: ( [A-Z0-9]+:)? logical replication sequence synchronization failed for subscription "regress_seq_sub"\n.*DETAIL:.* Missing sequence\(s\) on publisher: \("public.regress_s5"\)/,
+	$log_offset);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 93ad46f33c0..f8777a7009f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1627,6 +1627,7 @@ LogicalRepRelId
 LogicalRepRelMapEntry
 LogicalRepRelation
 LogicalRepRollbackPreparedTxnData
+LogicalRepSequenceInfo
 LogicalRepStreamAbortData
 LogicalRepTupleData
 LogicalRepTyp
-- 
2.43.0

