diff --git a/changelogs/unreleased/gh-9263-assertion-in-box-promote-qsync.md b/changelogs/unreleased/gh-9263-assertion-in-box-promote-qsync.md
new file mode 100644
index 0000000000000000000000000000000000000000..d5764e92fd187799bb5b36bbc6ee37fea42b7943
--- /dev/null
+++ b/changelogs/unreleased/gh-9263-assertion-in-box-promote-qsync.md
@@ -0,0 +1,8 @@
+## bugfix/core
+
+* Fixed a bug when the assertion in `box_promote_qsync` would fail in the
+  debug build mode. The assertion is that at the moment when `box_promote_qsync`
+  is called, no other promote is being executed. It turned out that this
+  assertion is basically incorrect. In the release build mode, this incorrect
+  assumption could potentially lead to writing 2 PROMOTE entries in the same
+  term (gh-9263).
diff --git a/src/box/box.cc b/src/box/box.cc
index b1b2812a8bfbd0384a51f09f339949e39d91e32f..74ddaef65bae728413e5609bdb0550111e99d1f6 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -2901,7 +2901,10 @@ box_issue_demote(int64_t promote_lsn)
 int
 box_promote_qsync(void)
 {
-	assert(!is_in_box_promote);
+	if (is_in_box_promote) {
+		diag_set(ClientError, ER_IN_ANOTHER_PROMOTE);
+		return -1;
+	}
 	assert(is_box_configured);
 	struct raft *raft = box_raft();
 	is_in_box_promote = true;
diff --git a/src/box/errcode.h b/src/box/errcode.h
index b4489691b5a0bac39f3375c7802088f71ce2d192..775bddd1e1b4fcbbe831e5887173bd4f85b35af1 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -330,6 +330,7 @@ struct errcode_record {
 	/*275 */_(ER_CREATE_DEFAULT_FUNC,	"Failed to create field default function '%s': %s") \
 	/*276 */_(ER_DEFAULT_FUNC_FAILED,	"Error calling field default function '%s': %s") \
 	/*277 */_(ER_INVALID_DEC,		"Invalid decimal: '%s'") \
+	/*278 */_(ER_IN_ANOTHER_PROMOTE,	"box.ctl.promote() is already running") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/raft.c b/src/box/raft.c
index 2f22b74668672db6357d4e19fc964d563ffcbe18..2becd6578da15017b89194ad36b0f1eef44f61fc 100644
--- a/src/box/raft.c
+++ b/src/box/raft.c
@@ -123,14 +123,22 @@ box_raft_update_synchro_queue(struct raft *raft)
 		return;
 	int rc = 0;
 	uint32_t errcode = 0;
+	bool try_again;
 	do {
+		try_again = false;
 		rc = box_promote_qsync();
 		if (rc != 0) {
 			struct error *err = diag_last_error(diag_get());
 			errcode = box_error_code(err);
 			diag_log();
+			if (!fiber_is_cancelled() &&
+			    (errcode == ER_QUORUM_WAIT ||
+			    errcode == ER_IN_ANOTHER_PROMOTE)) {
+				try_again = true;
+				fiber_sleep(0);
+			}
 		}
-	} while (rc != 0 && errcode == ER_QUORUM_WAIT && !fiber_is_cancelled());
+	} while (try_again);
 }
 
 static int
diff --git a/test/box/error.result b/test/box/error.result
index c39f3adcc72b9af5cc18b19778adeaf477405e4c..40a4102012562783be0ed8d13803004a1047a165 100644
--- a/test/box/error.result
+++ b/test/box/error.result
@@ -496,6 +496,7 @@ t;
  |   275: box.error.CREATE_DEFAULT_FUNC
  |   276: box.error.DEFAULT_FUNC_FAILED
  |   277: box.error.INVALID_DEC
+ |   278: box.error.IN_ANOTHER_PROMOTE
  | ...
 
 test_run:cmd("setopt delimiter ''");
diff --git a/test/replication-luatest/gh_9263_assertion_in_box_promote_qsync_test.lua b/test/replication-luatest/gh_9263_assertion_in_box_promote_qsync_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..ab527ca2216160b236db2b1efd8ef72b663d08c8
--- /dev/null
+++ b/test/replication-luatest/gh_9263_assertion_in_box_promote_qsync_test.lua
@@ -0,0 +1,151 @@
+local t = require('luatest')
+local cluster = require('luatest.replica_set')
+local proxy = require('luatest.replica_proxy')
+local server = require('luatest.server')
+
+local g = t.group('assertion-in-box-promote-qsync')
+
+local wait_timeout = 10
+
+local function wait_pair_sync(server1, server2)
+    -- Without retrying it fails sometimes when vclocks are empty and both
+    -- instances are in 'connect' state instead of 'follow'.
+    t.helpers.retrying({timeout = 10}, function()
+        server1:wait_for_vclock_of(server2)
+        server2:wait_for_vclock_of(server1)
+        server1:assert_follows_upstream(server2:get_instance_id())
+        server2:assert_follows_upstream(server1:get_instance_id())
+    end)
+end
+
+local function server_wait_wal_is_blocked(server)
+    server:exec(function()
+        t.helpers.retrying({timeout = 10}, function()
+            t.assert(box.error.injection.get('ERRINJ_WAL_DELAY'))
+        end)
+    end)
+end
+
+local function server_wait_synchro_queue_len_is_equal(server, expected)
+    server:exec(function(expected)
+        t.helpers.retrying({timeout = 10}, function(expected)
+            t.assert_equals(box.info.synchro.queue.len, expected)
+        end, expected)
+    end, {expected})
+end
+
+local function get_wait_quorum_count(server)
+    return server:exec(function()
+        return box.error.injection.get('ERRINJ_WAIT_QUORUM_COUNT')
+    end)
+end
+
+local function server_wait_wait_quorum_count_ge_than(server, threshold)
+    server:exec(function(threshold, wait_timeout)
+        t.helpers.retrying({timeout = wait_timeout}, function(threshold)
+            t.assert_ge(box.error.injection.get('ERRINJ_WAIT_QUORUM_COUNT'),
+                threshold)
+        end, threshold)
+    end, {threshold, wait_timeout})
+end
+
+g.before_each(function(cg)
+    t.tarantool.skip_if_not_debug()
+
+    cg.cluster = cluster:new({})
+
+    local box_cfg = {
+        replication = {
+            server.build_listen_uri('master', cg.cluster.id),
+            server.build_listen_uri('replica_proxy'),
+        },
+        election_mode = 'candidate',
+        replication_synchro_quorum = 2,
+        replication_synchro_timeout = 100000,
+        replication_timeout = 0.1,
+        election_fencing_mode = 'off',
+    }
+    cg.master = cg.cluster:build_and_add_server({
+        alias = 'master',
+        box_cfg = box_cfg
+    })
+    box_cfg.replication = {
+        server.build_listen_uri('replica', cg.cluster.id),
+        server.build_listen_uri('master_proxy'),
+    }
+    box_cfg.election_mode = 'off'
+    cg.replica = cg.cluster:build_and_add_server({
+        alias = 'replica',
+        box_cfg = box_cfg
+    })
+    cg.master_proxy = proxy:new({
+        client_socket_path = server.build_listen_uri('master_proxy'),
+        server_socket_path = server.build_listen_uri('master', cg.cluster.id),
+    })
+    t.assert(cg.master_proxy:start({force = true}))
+    cg.replica_proxy = proxy:new({
+        client_socket_path = server.build_listen_uri('replica_proxy'),
+        server_socket_path = server.build_listen_uri('replica', cg.cluster.id),
+    })
+    t.assert(cg.replica_proxy:start({force = true}))
+    cg.cluster:start()
+    cg.master:wait_until_election_leader_found()
+    cg.replica:wait_until_election_leader_found()
+
+    cg.master:exec(function()
+        box.schema.space.create('test', {is_sync = true})
+        box.space.test:create_index('pk')
+    end)
+    wait_pair_sync(cg.replica, cg.master)
+end)
+
+g.after_each(function(cg)
+    cg.cluster:drop()
+end)
+
+g.test_is_in_box_promote = function(cg)
+    local f = cg.replica:exec(function()
+        box.error.injection.set('ERRINJ_WAL_DELAY_COUNTDOWN', 0)
+        local f = require('fiber').create(function() box.ctl.promote() end)
+        f:set_joinable(true)
+        return f:id()
+    end)
+    server_wait_wal_is_blocked(cg.replica)
+
+    cg.replica_proxy:pause()
+
+    t.helpers.retrying({timeout = 10}, function()
+        cg.master:exec(function()
+            local status = box.info.replication[2].upstream.status
+            t.assert(status ~= 'follow')
+        end)
+    end)
+
+    cg.master:exec(function()
+        require('fiber').create(function() box.space.test:insert{1} end)
+    end)
+    server_wait_synchro_queue_len_is_equal(cg.replica, 1)
+
+    local wait_quorum_count = get_wait_quorum_count(cg.replica)
+    local ff = require('fiber').create(function()
+        cg.replica:exec(function(f)
+            box.error.injection.set('ERRINJ_WAL_DELAY', false)
+            local ok, _ = require('fiber').find(f):join()
+            t.assert(ok)
+        end, {f})
+    end)
+    ff:set_joinable(true)
+    -- We need to be sure we entered the 'box_wait_quorum' call.
+    server_wait_wait_quorum_count_ge_than(cg.replica, wait_quorum_count + 1)
+    cg.replica:exec(function()
+        box.cfg{
+            election_mode = 'candidate',
+            replication_synchro_quorum = 1
+        }
+    end)
+    cg.replica:wait_for_election_state('leader')
+    cg.replica_proxy:resume()
+    local ok, err = ff:join()
+    t.assert_equals(err, nil)
+    t.assert(ok)
+end