diff --git a/src/box/box.cc b/src/box/box.cc
index ef048d4a7cf8abe98d1e4d3f5599bc20ec873752..38312ae23c626124e462ab9e81193c740eda4d68 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -81,7 +81,7 @@ process_rw(struct port *port, struct request *request)
 		request->execute(request, port);
 		port_eof(port);
 	} catch (Exception *e) {
-		txn_rollback();
+		txn_rollback_stmt();
 		throw;
 	}
 }
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index f7c10d78e9daf0ccb1f0f578444007d092195830..cebfe88ad7f03b2daa3a7ff472dbb2b31545fb0f 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -45,6 +45,14 @@ ffi.cdef[[
               const char *key, const char *key_end);
     void password_prepare(const char *password, int len,
 		                  char *out, int out_len);
+    int
+    boxffi_txn_begin();
+
+    int
+    boxffi_txn_commit();
+
+    void
+    boxffi_txn_rollback();
 ]]
 
 local function user_resolve(user)
@@ -61,6 +69,10 @@ local function user_resolve(user)
     return tuple[1]
 end
 
+box.begin = function() if ffi.C.boxffi_txn_begin() == -1 then box.raise() end end
+box.commit = function() if ffi.C.boxffi_txn_commit() == -1 then box.raise() end end
+box.rollback = ffi.C.boxffi_txn_rollback;
+
 box.schema.space = {}
 box.schema.space.create = function(name, options)
     local _space = box.space[box.schema.SPACE_ID]
diff --git a/src/box/txn.cc b/src/box/txn.cc
index 24158fc352b2363b59dc6803404bd318a8fdf908..42d343cb74dde847c128efea2de8b1024c3654d8 100644
--- a/src/box/txn.cc
+++ b/src/box/txn.cc
@@ -174,6 +174,32 @@ txn_commit(struct txn *txn)
 	in_txn() = NULL;
 }
 
+/**
+ * Void all effects of the statement, but
+ * keep it in the list - to maintain
+ * limit on the number of statements in a
+ * trasnaction.
+ */
+void
+txn_rollback_stmt()
+{
+	struct txn *txn = in_txn();
+	if (txn == NULL)
+		return;
+	if (txn->autocommit)
+		return txn_rollback();
+	struct txn_stmt *stmt = txn_stmt(txn);
+	if (stmt->old_tuple || stmt->new_tuple) {
+		space_replace(stmt->space, stmt->new_tuple,
+			      stmt->old_tuple, DUP_INSERT);
+		if (stmt->new_tuple)
+			tuple_ref(stmt->new_tuple, -1);
+	}
+	stmt->old_tuple = stmt->new_tuple = NULL;
+	stmt->space = NULL;
+	stmt->row = NULL;
+}
+
 void
 txn_rollback()
 {
@@ -206,3 +232,40 @@ txn_check_autocommit(struct txn *txn, const char *where)
 			  where, "multi-statement transactions");
 	}
 }
+
+extern "C" {
+
+int
+boxffi_txn_begin()
+{
+	try {
+		if (in_txn())
+			tnt_raise(ClientError, ER_ACTIVE_TRANSACTION);
+		(void) txn_begin(false);
+	} catch (Exception  *e) {
+		return -1; /* pass exception  through FFI */
+	}
+	return 0;
+}
+
+int
+boxffi_txn_commit()
+{
+	try {
+		struct txn *txn = in_txn();
+		if (txn == NULL)
+			tnt_raise(ClientError, ER_NO_ACTIVE_TRANSACTION);
+		txn_commit(txn);
+	} catch (Exception  *e) {
+		return -1; /* pass exception through FFI */
+	}
+	return 0;
+}
+
+void
+boxffi_txn_rollback()
+{
+	txn_rollback(); /* doesn't throw */
+}
+
+} /* extern "C" */
diff --git a/src/box/txn.h b/src/box/txn.h
index 06cd9c20ba9a631cfee9375817764771b09bdbe3..3f9870b5fbb1b60e5a9450b8fb73bf780daad037 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -85,6 +85,13 @@ txn_begin_stmt(struct request *request);
 void
 txn_commit_stmt(struct txn *txn, struct port *port);
 
+/**
+ * Rollback a statement. In autocommit mode,
+ * rolls back the entire transaction.
+ */
+void
+txn_rollback_stmt();
+
 /**
  * Start a transaction explicitly.
  * @pre no transaction is active
@@ -123,4 +130,30 @@ txn_stmt(struct txn *txn)
 	return rlist_last_entry(&txn->stmts, struct txn_stmt, next);
 }
 
+/**
+ * FFI bindings: do not throw exceptions, do not accept extra
+ * arguments
+ */
+extern "C" {
+
+/**
+ * @retval 0 - success
+ * @retval -1 - failed, perhaps a transaction has already been
+ * started
+ */
+int
+boxffi_txn_begin();
+
+/**
+ * @retval 0 - success
+ * @retval -1 - commit failed
+ */
+int
+boxffi_txn_commit();
+
+void
+boxffi_txn_rollback();
+
+} /* extern  "C" */
+
 #endif /* TARANTOOL_BOX_TXN_H_INCLUDED */
diff --git a/src/errcode.h b/src/errcode.h
index 052b02d631afdb14b21a1d016a8a30ee93c1ce9e..82ee385c324149861813149e3d07435bb70b2e4f 100644
--- a/src/errcode.h
+++ b/src/errcode.h
@@ -128,6 +128,8 @@ enum { TNT_ERRMSG_MAX = 512 };
 	/* 76 */_(ER_INVALID_XLOG_ORDER,	2, "Invalid xlog order: %lld and %lld") \
 	/* 77 */_(ER_NO_CONNECTION,		2, "Connection is not established") \
 	/* 78 */_(ER_TIMEOUT,			2, "Timeout exceeded") \
+	/* 79 */_(ER_ACTIVE_TRANSACTION,	2, "Operation is not permitted when there is an active transaction ") \
+	/* 80 */_(ER_NO_ACTIVE_TRANSACTION,	2, "Operation is not permitted when there is no active transaction ") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/session.cc b/src/session.cc
index 7a089fc6626f7a5e9cfbccea7fbc406288f16d70..788933811d17ba6e33b223763672f60f2b2157f4 100644
--- a/src/session.cc
+++ b/src/session.cc
@@ -35,6 +35,7 @@
 #include "exception.h"
 #include "random.h"
 #include <sys/socket.h>
+#include "box/txn.h"
 
 static struct mh_i32ptr_t *session_registry;
 
@@ -104,6 +105,10 @@ session_run_on_connect_triggers(struct session *session)
 void
 session_destroy(struct session *session)
 {
+	if (session->txn) {
+		assert(session->txn == in_txn());
+		txn_rollback();
+	}
 	assert(session->txn == NULL);
 	struct mh_i32ptr_node_t node = { session->id, NULL };
 	mh_i32ptr_remove(session_registry, &node, NULL);
diff --git a/test/box/misc.result b/test/box/misc.result
index 0e61e0aaa0fcd8aba6aca5db026e36089123ba83..ca8685af9e44dc9da165d1fc1836c74f382a1641 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -16,11 +16,14 @@ t = {} for n in pairs(box) do table.insert(t, tostring(n)) end table.sort(t)
 ...
 t
 ---
-- - cfg
+- - begin
+  - cfg
+  - commit
   - error
   - index
   - info
   - raise
+  - rollback
   - schema
   - slab
   - snapshot
@@ -197,12 +200,14 @@ t;
   - 'box.error.CREATE_USER : 43'
   - 'box.error.CREATE_SPACE : 9'
   - 'box.error.UNKNOWN_SCHEMA_OBJECT : 49'
+  - 'box.error.NO_ACTIVE_TRANSACTION : 80'
+  - 'box.error.SPLICE : 25'
   - 'box.error.FIELD_TYPE_MISMATCH : 24'
   - 'box.error.UNSUPPORTED : 5'
   - 'box.error.INVALID_MSGPACK : 20'
   - 'box.error.KEY_PART_COUNT : 31'
   - 'box.error.ALTER_SPACE : 12'
-  - 'box.error.SPLICE : 25'
+  - 'box.error.ACTIVE_TRANSACTION : 79'
   - 'box.error.NO_CONNECTION : 77'
   - 'box.error.TUPLE_FOUND : 3'
   - 'box.error.INVALID_XLOG_NAME : 75'
diff --git a/test/box/transaction.result b/test/box/transaction.result
new file mode 100644
index 0000000000000000000000000000000000000000..c2f2e28ac6d1434618d4d567585011be348cc692
--- /dev/null
+++ b/test/box/transaction.result
@@ -0,0 +1,180 @@
+--# setopt delimiter ';'
+-- empty transaction - ok
+box.begin() box.commit();
+---
+...
+-- double begin
+box.begin() box.begin();
+---
+- error: 'Operation is not permitted when there is an active transaction '
+...
+-- no active transaction since exception rolled it back
+box.commit();
+---
+...
+-- double commit - error
+box.begin() box.commit() box.commit();
+---
+- error: 'Operation is not permitted when there is no active transaction '
+...
+-- commit if not started - error
+box.commit();
+---
+- error: 'Operation is not permitted when there is no active transaction '
+...
+-- rollback if not started - ok
+-- double rollback - ok
+box.rollback()
+box.begin() box.rollback() box.rollback();
+---
+...
+-- rollback of an empty trans - ends transaction
+box.begin() box.rollback();
+---
+...
+-- no current transaction - error
+box.commit();
+---
+- error: 'Operation is not permitted when there is no active transaction '
+...
+fiber = require('fiber');
+---
+...
+function sloppy()
+    box.begin()
+end;
+---
+...
+f = fiber.wrap(sloppy);
+---
+...
+-- when the sloppy fiber ends, its session has an active transction
+-- ensure it's rolled back automatically
+fiber.sleep(0);
+---
+...
+fiber.sleep(0);
+---
+...
+-- transactions and system spaces
+box.begin() box.schema.space.create('test');
+---
+- error: Space _space does not support multi-statement transactions
+...
+box.rollback();
+---
+...
+box.begin() box.schema.func.create('test');
+---
+- error: Space _func does not support multi-statement transactions
+...
+box.rollback();
+---
+...
+box.begin() box.schema.user.create('test');
+---
+- error: Space _user does not support multi-statement transactions
+...
+box.rollback();
+---
+...
+box.begin() box.schema.user.grant('guest', 'read', 'universe');
+---
+- error: Space _priv does not support multi-statement transactions
+...
+box.rollback();
+---
+...
+box.begin() box.space._schema:insert{'test'};
+---
+- error: Space _schema does not support multi-statement transactions
+...
+box.rollback();
+---
+...
+box.begin() box.space._cluster:insert{123456789, 'abc'};
+---
+- error: Space _cluster does not support multi-statement transactions
+...
+box.rollback();
+---
+...
+s = box.schema.space.create('test');
+---
+...
+box.begin() s:create_index('primary');
+---
+- error: Space _index does not support multi-statement transactions
+...
+box.rollback();
+---
+...
+s:create_index('primary');
+---
+...
+function multi()
+    box.begin()
+    s:auto_increment{'first row'}
+    s:auto_increment{'second row'}
+    t = s:select{}
+    box.commit()
+end;
+---
+...
+multi();
+---
+...
+t;
+---
+- - [1, 'first row']
+  - [2, 'second row']
+...
+s:select{};
+---
+- - [1, 'first row']
+  - [2, 'second row']
+...
+s:truncate();
+---
+...
+function multi()
+    box.begin()
+    s:auto_increment{'first row'}
+    s:auto_increment{'second row'}
+    t = s:select{}
+    box.rollback()
+end;
+---
+...
+multi();
+---
+...
+t;
+---
+- - [1, 'first row']
+  - [2, 'second row']
+...
+s:select{};
+---
+- []
+...
+function multi()
+    box.begin()
+    s:insert{1, 'first row'}
+    pcall(s.insert, s, {1, 'duplicate'})
+    t = s:select{}
+    box.commit()
+end;
+---
+...
+multi();
+---
+...
+t;
+---
+- - [1, 'first row']
+...
+s:select{};
+---
+- - [1, 'first row']
+...
diff --git a/test/box/transaction.test.lua b/test/box/transaction.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..981430f48d8d9e886f5cbb645f32eb1fc3f352b6
--- /dev/null
+++ b/test/box/transaction.test.lua
@@ -0,0 +1,76 @@
+--# setopt delimiter ';'
+-- empty transaction - ok
+box.begin() box.commit();
+-- double begin
+box.begin() box.begin();
+-- no active transaction since exception rolled it back
+box.commit();
+-- double commit - error
+box.begin() box.commit() box.commit();
+-- commit if not started - error
+box.commit();
+-- rollback if not started - ok
+box.rollback()
+-- double rollback - ok
+box.begin() box.rollback() box.rollback();
+-- rollback of an empty trans - ends transaction
+box.begin() box.rollback();
+-- no current transaction - error
+box.commit();
+fiber = require('fiber');
+function sloppy()
+    box.begin()
+end;
+f = fiber.wrap(sloppy);
+-- when the sloppy fiber ends, its session has an active transction
+-- ensure it's rolled back automatically
+fiber.sleep(0);
+fiber.sleep(0);
+-- transactions and system spaces
+box.begin() box.schema.space.create('test');
+box.rollback();
+box.begin() box.schema.func.create('test');
+box.rollback();
+box.begin() box.schema.user.create('test');
+box.rollback();
+box.begin() box.schema.user.grant('guest', 'read', 'universe');
+box.rollback();
+box.begin() box.space._schema:insert{'test'};
+box.rollback();
+box.begin() box.space._cluster:insert{123456789, 'abc'};
+box.rollback();
+s = box.schema.space.create('test');
+box.begin() s:create_index('primary');
+box.rollback();
+s:create_index('primary');
+function multi()
+    box.begin()
+    s:auto_increment{'first row'}
+    s:auto_increment{'second row'}
+    t = s:select{}
+    box.commit()
+end;
+multi();
+t;
+s:select{};
+s:truncate();
+function multi()
+    box.begin()
+    s:auto_increment{'first row'}
+    s:auto_increment{'second row'}
+    t = s:select{}
+    box.rollback()
+end;
+multi();
+t;
+s:select{};
+function multi()
+    box.begin()
+    s:insert{1, 'first row'}
+    pcall(s.insert, s, {1, 'duplicate'})
+    t = s:select{}
+    box.commit()
+end;
+multi();
+t;
+s:select{};