diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index 8f899fed8c1dab9eac2eea2ca80d7703d9ca18c9..b98f504d3269808eb83fec0b96ddce30904979a3 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -296,8 +296,13 @@ static const struct cmsg_hop destroy_route[] = {
 static void
 tx_process_disconnect(struct cmsg *m);
 
+/** Send destroy message to tx thread. */
+static void
+net_finish_disconnect(struct cmsg *m);
+
 static const struct cmsg_hop disconnect_route[] = {
-	{ tx_process_disconnect, NULL }
+	{ tx_process_disconnect, &net_pipe },
+	{ net_finish_disconnect, NULL }
 };
 
 /**
@@ -327,6 +332,32 @@ static const struct cmsg_hop push_route[] = {
 
 /* {{{ iproto_connection - declaration and definition */
 
+/** Connection life cycle stages. */
+enum iproto_connection_state {
+	/**
+	 * A connection is always alive in the beginning because
+	 * takes an already active socket in a constructor.
+	 */
+	IPROTO_CONNECTION_ALIVE,
+	/**
+	 * Socket was closed, a notification is sent to the TX
+	 * thread to close the session.
+	 */
+	IPROTO_CONNECTION_CLOSED,
+	/**
+	 * TX thread was notified about close, but some requests
+	 * are still not finished. That state may be skipped in
+	 * case the connection was already idle (not having
+	 * unfinished requests) at the moment of closing.
+	 */
+	IPROTO_CONNECTION_PENDING_DESTROY,
+	/**
+	 * All requests are finished, a destroy request is sent to
+	 * the TX thread.
+	 */
+	IPROTO_CONNECTION_DESTROYED,
+};
+
 /**
  * Context of a single client connection.
  * Interaction scheme:
@@ -430,8 +461,12 @@ struct iproto_connection
 	 * connection.
 	 */
 	struct cmsg destroy_msg;
-	/** True if destroy message is sent. Debug-only. */
-	bool is_destroy_sent;
+	/**
+	 * Connection state. Mainly it is used to determine when
+	 * the connection can be destroyed, and for debug purposes
+	 * to assert on a double destroy, for example.
+	 */
+	enum iproto_connection_state state;
 	struct rlist in_stop_list;
 	/**
 	 * Kharon is used to implement box.session.push().
@@ -572,6 +607,35 @@ iproto_connection_stop_msg_max_limit(struct iproto_connection *con)
 	rlist_add_tail(&stopped_connections, &con->in_stop_list);
 }
 
+/**
+ * Send a destroy message to TX thread in case all requests are
+ * finished.
+ */
+static inline void
+iproto_connection_try_to_start_destroy(struct iproto_connection *con)
+{
+	assert(con->state == IPROTO_CONNECTION_CLOSED ||
+	       con->state == IPROTO_CONNECTION_PENDING_DESTROY);
+	if (!iproto_connection_is_idle(con)) {
+		/*
+		 * Not all requests are finished. Let the last
+		 * finished request destroy the connection.
+		 */
+		con->state = IPROTO_CONNECTION_PENDING_DESTROY;
+		return;
+	}
+	/*
+	 * If the connection has no outstanding requests in the
+	 * input buffer, then no one (e.g. tx thread) is referring
+	 * to it, so it must be destroyed. Firstly queue a msg to
+	 * destroy the session and other resources owned by TX
+	 * thread. When it is done, iproto thread will destroy
+	 * other parts of the connection.
+	 */
+	con->state = IPROTO_CONNECTION_DESTROYED;
+	cpipe_push(&tx_pipe, &con->destroy_msg);
+}
+
 /**
  * Initiate a connection shutdown. This method may
  * be invoked many times, and does the internal
@@ -597,23 +661,12 @@ iproto_connection_close(struct iproto_connection *con)
 		 */
 		con->p_ibuf->wpos -= con->parse_size;
 		cpipe_push(&tx_pipe, &con->disconnect_msg);
-	}
-	/*
-	 * If the connection has no outstanding requests in the
-	 * input buffer, then no one (e.g. tx thread) is referring
-	 * to it, so it must be destroyed at once. Queue a msg to
-	 * run on_disconnect() trigger and destroy the connection.
-	 *
-	 * Otherwise, it will be destroyed by the last request on
-	 * this connection that has finished processing.
-	 *
-	 * The check is mandatory to not destroy a connection
-	 * twice.
-	 */
-	if (iproto_connection_is_idle(con)) {
-		assert(! con->is_destroy_sent);
-		con->is_destroy_sent = true;
-		cpipe_push(&tx_pipe, &con->destroy_msg);
+		assert(con->state == IPROTO_CONNECTION_ALIVE);
+		con->state = IPROTO_CONNECTION_CLOSED;
+	} else if (con->state == IPROTO_CONNECTION_PENDING_DESTROY) {
+		iproto_connection_try_to_start_destroy(con);
+	} else {
+		assert(con->state == IPROTO_CONNECTION_CLOSED);
 	}
 	rlist_del(&con->in_stop_list);
 }
@@ -1048,7 +1101,7 @@ iproto_connection_new(int fd)
 	/* It may be very awkward to allocate at close. */
 	cmsg_init(&con->destroy_msg, destroy_route);
 	cmsg_init(&con->disconnect_msg, disconnect_route);
-	con->is_destroy_sent = false;
+	con->state = IPROTO_CONNECTION_ALIVE;
 	con->tx.is_push_pending = false;
 	con->tx.is_push_sent = false;
 	rmean_collect(rmean_net, IPROTO_CONNECTIONS, 1);
@@ -1063,6 +1116,7 @@ iproto_connection_delete(struct iproto_connection *con)
 	assert(!evio_has_fd(&con->output));
 	assert(!evio_has_fd(&con->input));
 	assert(con->session == NULL);
+	assert(con->state == IPROTO_CONNECTION_DESTROYED);
 	/*
 	 * The output buffers must have been deleted
 	 * in tx thread.
@@ -1281,6 +1335,14 @@ tx_process_disconnect(struct cmsg *m)
 	}
 }
 
+static void
+net_finish_disconnect(struct cmsg *m)
+{
+	struct iproto_connection *con =
+		container_of(m, struct iproto_connection, disconnect_msg);
+	iproto_connection_try_to_start_destroy(con);
+}
+
 /**
  * Destroy the session object, as well as output buffers of the
  * connection.
diff --git a/test/box/gh-4627-session-use-after-free.result b/test/box/gh-4627-session-use-after-free.result
new file mode 100644
index 0000000000000000000000000000000000000000..5e5c154b92dc63714c2a0eeb161d943e4ea7ba98
--- /dev/null
+++ b/test/box/gh-4627-session-use-after-free.result
@@ -0,0 +1,60 @@
+-- test-run result file version 2
+--
+-- gh-4627: binary session disconnect trigger yield could lead to
+-- use after free of the session object. That happened because
+-- iproto thread sent two requests to TX thread at disconnect:
+--
+--     - Close the session and run its on disconnect triggers;
+--
+--     - If all requests are handled, destroy the session.
+--
+-- When a connection is idle, all requests are handled, so both
+-- these requests are sent. If the first one yielded in TX thread,
+-- the second one arrived and destroyed the session right under
+-- the feet of the first one.
+--
+net_box = require('net.box')
+ | ---
+ | ...
+fiber = require('fiber')
+ | ---
+ | ...
+
+sid_before_yield = nil
+ | ---
+ | ...
+sid_after_yield = nil
+ | ---
+ | ...
+func = box.session.on_disconnect(function()     \
+    sid_before_yield = box.session.id()         \
+    fiber.yield()                               \
+    sid_after_yield = box.session.id()          \
+end)
+ | ---
+ | ...
+
+connection = net_box.connect(box.cfg.listen)
+ | ---
+ | ...
+connection:ping()
+ | ---
+ | - true
+ | ...
+connection:close()
+ | ---
+ | ...
+
+while not sid_after_yield do fiber.yield() end
+ | ---
+ | ...
+
+sid_after_yield == sid_before_yield and sid_after_yield ~= 0 or \
+    {sid_after_yield, sid_before_yield}
+ | ---
+ | - true
+ | ...
+
+box.session.on_disconnect(nil, func)
+ | ---
+ | ...
diff --git a/test/box/gh-4627-session-use-after-free.test.lua b/test/box/gh-4627-session-use-after-free.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..70624a96a4e0d0edc2da961f9a76def49f16e7e6
--- /dev/null
+++ b/test/box/gh-4627-session-use-after-free.test.lua
@@ -0,0 +1,35 @@
+--
+-- gh-4627: binary session disconnect trigger yield could lead to
+-- use after free of the session object. That happened because
+-- iproto thread sent two requests to TX thread at disconnect:
+--
+--     - Close the session and run its on disconnect triggers;
+--
+--     - If all requests are handled, destroy the session.
+--
+-- When a connection is idle, all requests are handled, so both
+-- these requests are sent. If the first one yielded in TX thread,
+-- the second one arrived and destroyed the session right under
+-- the feet of the first one.
+--
+net_box = require('net.box')
+fiber = require('fiber')
+
+sid_before_yield = nil
+sid_after_yield = nil
+func = box.session.on_disconnect(function()     \
+    sid_before_yield = box.session.id()         \
+    fiber.yield()                               \
+    sid_after_yield = box.session.id()          \
+end)
+
+connection = net_box.connect(box.cfg.listen)
+connection:ping()
+connection:close()
+
+while not sid_after_yield do fiber.yield() end
+
+sid_after_yield == sid_before_yield and sid_after_yield ~= 0 or \
+    {sid_after_yield, sid_before_yield}
+
+box.session.on_disconnect(nil, func)