diff --git a/src/luamod.rs b/src/luamod.rs
index f3979782b8e63f44c5d0155fdd327b55863bd6d4..86cdd25c98a85da4f119e6b95dbd820d11ad5d5a 100644
--- a/src/luamod.rs
+++ b/src/luamod.rs
@@ -647,11 +647,11 @@ pub(crate) fn setup(args: &args::Run) {
 
             let node = traft::node::global()?;
             let mut node_impl = node.node_impl();
-            let index = node_impl.propose(op)?;
+            let entry_id = node_impl.propose_async(op)?;
             node.main_loop.wakeup();
             // Release the lock
             drop(node_impl);
-            Ok(index)
+            Ok(entry_id.index)
         }),
     );
 
diff --git a/src/traft/node.rs b/src/traft/node.rs
index 0fb14f4fce5cb3dd92133420e0fa731a37bfaa01..0c77cb046d8a751d16021b6359b5acde09ee2378 100644
--- a/src/traft/node.rs
+++ b/src/traft/node.rs
@@ -517,14 +517,24 @@ impl NodeImpl {
     ///
     /// Returns id of the proposed entry, which can be used to await it's
     /// application.
-    /// NOTE: the entry may not actually be committed, and the entry at that
+    ///
+    /// Returns an error if current instance is not a raft leader, because
+    /// followers should propose raft log entries via [`proc_cas`].
+    ///
+    /// NOTE: the proposed entry may still be dropped, and the entry at that
     /// index may be some other one. It's the callers responsibility to verify
     /// which entry got committed.
+    ///
+    /// [`proc_cas`]: crate::cas::proc_cas
     #[inline]
-    pub fn propose_async<T>(&mut self, op: T) -> Result<RaftEntryId, RaftError>
+    pub fn propose_async<T>(&mut self, op: T) -> Result<RaftEntryId, Error>
     where
         T: Into<Op>,
     {
+        if self.raw_node.raft.state != RaftStateRole::Leader {
+            return Err(Error::NotALeader);
+        }
+
         let index_before = self.raw_node.raft.raft_log.last_index();
 
         let ctx = traft::EntryContext::Op(op.into());
@@ -538,17 +548,6 @@ impl NodeImpl {
         Ok(entry_id)
     }
 
-    /// Proposes a raft entry to be appended to the log and returns raft index
-    /// at which it is expected to be committed unless it gets rejected.
-    ///
-    /// **Doesn't yield**
-    pub fn propose(&mut self, op: Op) -> Result<RaftIndex, RaftError> {
-        let ctx = traft::EntryContext::Op(op);
-        self.raw_node.propose(ctx.into_raft_ctx(), vec![])?;
-        let index = self.raw_node.raft.raft_log.last_index();
-        Ok(index)
-    }
-
     pub fn campaign(&mut self) -> Result<(), RaftError> {
         self.raw_node.campaign()
     }
diff --git a/test/int/test_couple.py b/test/int/test_couple.py
index 0674e7a3857c48ec6a7b02d1adcecf58866f3067..ce5541b932417603622a1e40f88c4b6117d29ed3 100644
--- a/test/int/test_couple.py
+++ b/test/int/test_couple.py
@@ -8,15 +8,6 @@ def cluster2(cluster: Cluster):
     return cluster
 
 
-def test_follower_proposal(cluster2: Cluster):
-    i1, i2 = cluster2.instances
-    i1.promote_or_fail()
-
-    i2.assert_raft_status("Follower", leader_id=i1.raft_id)
-
-    i2.raft_propose_nop()
-
-
 def test_switchover(cluster2: Cluster):
     i1, i2 = cluster2.instances
 
@@ -76,7 +67,6 @@ def test_restart_leader(cluster2: Cluster):
     i1.restart()
     i1.wait_online()
     assert i1.current_grade() == dict(variant="Online", incarnation=2)
-    i1.raft_propose_nop()
 
     i1.restart()
     i1.wait_online()