diff --git a/src/rpc/ddl_apply.rs b/src/rpc/ddl_apply.rs
index 081bc3726a52b333c61b900a79c4ea37806bf164..f874d8e4be483152f2b13d05c9b2f0868fb23bd1 100644
--- a/src/rpc/ddl_apply.rs
+++ b/src/rpc/ddl_apply.rs
@@ -7,7 +7,7 @@ use crate::traft::node;
 use crate::traft::Result;
 use crate::traft::{RaftIndex, RaftTerm};
 use std::time::Duration;
-use tarantool::error::TarantoolError;
+use tarantool::error::{TarantoolError, TarantoolErrorCode};
 use tarantool::ffi::tarantool as ffi;
 use tarantool::space::{Space, SystemSpace};
 
@@ -25,6 +25,14 @@ crate::define_rpc_request! {
             return Ok(Response::Ok);
         }
 
+        if crate::tarantool::eval("return box.info.ro")? {
+            let e = tarantool::set_and_get_error!(
+                TarantoolErrorCode::Readonly,
+                "cannot apply schema change on a read only instance"
+            );
+            return Err(e.into());
+        }
+
         let ddl = storage.properties.pending_schema_change()?.ok_or_else(|| Error::other("pending schema change not found"))?;
 
         // FIXME: start_transaction api is awful, it would be too ugly to
diff --git a/src/traft/error.rs b/src/traft/error.rs
index 7d9f5ce7664f468f20ebce4916e712fbd2f51371..8884a7495cbcb674c5e6b167c6a180c6c1c6902c 100644
--- a/src/traft/error.rs
+++ b/src/traft/error.rs
@@ -104,6 +104,12 @@ impl From<::tarantool::error::TransactionError> for Error {
     }
 }
 
+impl From<::tarantool::error::TarantoolError> for Error {
+    fn from(err: ::tarantool::error::TarantoolError) -> Self {
+        Self::Tarantool(err.into())
+    }
+}
+
 #[derive(Debug, Error)]
 pub enum CoercionError {
     #[error("unknown entry type ({0})")]