diff --git a/src/box/lua/xlog.c b/src/box/lua/xlog.c
index bd30187ae96790ac690cf8a90c77f3e4143cf2d9..971a26afedf9aa4c7a86009797b09f6f15564544 100644
--- a/src/box/lua/xlog.c
+++ b/src/box/lua/xlog.c
@@ -221,15 +221,19 @@ lbox_xlog_parser_iterate(struct lua_State *L)
 		lua_pushnumber(L, row.tm);
 		lua_settable(L, -3); /* timestamp */
 	}
-	if (row.txn_id != row.lsn || !row.is_commit) {
-		lua_pushstring(L, "txn_id");
-		lua_pushnumber(L, row.txn_id);
-		lua_settable(L, -3); /* txn_id */
+	if (row.tsn != row.lsn || !row.is_commit) {
+		lua_pushstring(L, "tsn");
+		lua_pushnumber(L, row.tsn);
+		lua_settable(L, -3); /* transaction identifier */
 	}
-	if (row.is_commit && row.txn_id != row.lsn) {
+	if (row.is_commit && row.tsn != row.lsn) {
 		lua_pushstring(L, "commit");
 		lua_pushboolean(L, true);
-		lua_settable(L, -3); /* txn_commit flag */
+		/*
+		 * is_commit, set for last row in multi-statement
+		 * transaction
+		 */
+		lua_settable(L, -3);
 	}
 
 	lua_settable(L, -3); /* HEADER */
diff --git a/src/box/txn.c b/src/box/txn.c
index 7f4e85b4786a9bf6d1eac36fab7870d4743ea15e..7900fb3ab2b6d2598ef976deb234b0d60e2dbdf1 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -131,7 +131,7 @@ txn_rollback_to_svp(struct txn *txn, struct stailq_entry *svp)
 struct txn *
 txn_begin(bool is_autocommit)
 {
-	static int64_t txn_id = 0;
+	static int64_t tsn = 0;
 	assert(! in_txn());
 	struct txn *txn = region_alloc_object(&fiber()->gc, struct txn);
 	if (txn == NULL) {
@@ -145,7 +145,7 @@ txn_begin(bool is_autocommit)
 	txn->has_triggers  = false;
 	txn->is_aborted = false;
 	txn->in_sub_stmt = 0;
-	txn->id = ++txn_id;
+	txn->id = ++tsn;
 	txn->signature = -1;
 	txn->engine = NULL;
 	txn->engine_tx = NULL;
diff --git a/src/box/wal.c b/src/box/wal.c
index 235a6f84099ca81f1a3593e18d9c67d7f94e271b..3faad9c3d2859e087b14c54afb336c85fc0f564b 100644
--- a/src/box/wal.c
+++ b/src/box/wal.c
@@ -895,16 +895,16 @@ wal_assign_lsn(struct vclock *vclock_diff, struct vclock *base,
 	       struct xrow_header **row,
 	       struct xrow_header **end)
 {
-	int64_t txn_id = 0;
+	int64_t tsn = 0;
 	/** Assign LSN to all local rows. */
 	for ( ; row < end; row++) {
 		if ((*row)->replica_id == 0) {
 			(*row)->lsn = vclock_inc(vclock_diff, instance_id) +
 				      vclock_get(base, instance_id);
 			(*row)->replica_id = instance_id;
-			/* Use the lsn of the first local row as txn_id. */
-			txn_id = txn_id == 0 ? (*row)->lsn : txn_id;
-			(*row)->txn_id = txn_id;
+			/* Use lsn of the first local row as transaction id. */
+			tsn = tsn == 0 ? (*row)->lsn : tsn;
+			(*row)->tsn = tsn;
 			(*row)->is_commit = row == end - 1;
 		} else {
 			vclock_follow(vclock_diff, (*row)->replica_id,
diff --git a/src/box/xrow.c b/src/box/xrow.c
index 46e7c7b63accbd2e456ff1c072098bddadffd17e..107009c90a76a6450dc63826e089a3807616fdd4 100644
--- a/src/box/xrow.c
+++ b/src/box/xrow.c
@@ -137,7 +137,7 @@ xrow_header_decode(struct xrow_header *header, const char **pos,
 			break;
 		case IPROTO_TSN:
 			has_tsn = true;
-			header->txn_id = mp_decode_uint(pos);
+			header->tsn = mp_decode_uint(pos);
 			break;
 		case IPROTO_FLAGS:
 			flags = mp_decode_uint(pos);
@@ -157,7 +157,7 @@ xrow_header_decode(struct xrow_header *header, const char **pos,
 		header->is_commit = true;
 	}
 	/* Restore transaction id from lsn and transaction serial number. */
-	header->txn_id = header->lsn - header->txn_id;
+	header->tsn = header->lsn - header->tsn;
 
 	/* Nop requests aren't supposed to have a body. */
 	if (*pos < end && header->type != IPROTO_NOP) {
@@ -243,8 +243,21 @@ xrow_header_encode(const struct xrow_header *header, uint64_t sync,
 		d = mp_encode_double(d, header->tm);
 		map_size++;
 	}
-	if (header->txn_id != 0) {
-		if (header->txn_id != header->lsn || !header->is_commit) {
+	/*
+	 * We do not encode tsn and is_commit flags for
+	 * single-statement transactions to save space in the
+	 * binary log. We also encode tsn as a diff from lsn
+	 * to save space in every multi-statement transaction row.
+	 * The rules when encoding are simple:
+	 * - if tsn is *not* encoded, it's a single-statement
+	 *   transaction, tsn = lsn, is_commit = true
+	 * - if tsn is present, it's a multi-statement
+	 *   transaction, tsn = tsn + lsn, check is_commit
+	 *   flag to find transaction boundary (last row in the
+	 *   transaction stream).
+	 */
+	if (header->tsn != 0) {
+		if (header->tsn != header->lsn || !header->is_commit) {
 			/*
 			 * Encode a transaction identifier for multi row
 			 * transaction members.
@@ -254,10 +267,10 @@ xrow_header_encode(const struct xrow_header *header, uint64_t sync,
 			 * Differential encoding: write a transaction serial
 			 * number (it is equal to lsn - transaction id) instead.
 			 */
-			d = mp_encode_uint(d, header->lsn - header->txn_id);
+			d = mp_encode_uint(d, header->lsn - header->tsn);
 			map_size++;
 		}
-		if (header->is_commit && header->txn_id != header->lsn) {
+		if (header->is_commit && header->tsn != header->lsn) {
 			/* Setup last row for multi row transaction. */
 			d = mp_encode_uint(d, IPROTO_FLAGS);
 			d = mp_encode_uint(d, IPROTO_FLAG_COMMIT);
diff --git a/src/box/xrow.h b/src/box/xrow.h
index ccd57f8b2c45d146b61f6df50ff541f942fa4544..e37da1e0faca7536c5eaa3715a3040bc643fe39f 100644
--- a/src/box/xrow.h
+++ b/src/box/xrow.h
@@ -59,11 +59,32 @@ struct xrow_header {
 
 	uint32_t type;
 	uint32_t replica_id;
+	/**
+	 * Replication group identifier. 0 - replicaset,
+	 * 1 - replica-local.
+         */
 	uint32_t group_id;
 	uint64_t sync;
-	int64_t lsn; /* LSN must be signed for correct comparison */
+	/** Log sequence number.
+	 * LSN must be signed for correct comparison
+	 */
+	int64_t lsn;
+	/** Timestamp. Used only when writing to the write ahead
+	 * log.
+	 */
 	double tm;
-	int64_t txn_id;
+	/*
+	 * Transaction identifier. LSN of the first row in the
+	 * transaction.
+	 */
+	int64_t tsn;
+	/**
+	 * True for the last row in a multi-statement transaction,
+	 * or single-statement transaction. Is only encoded in the
+	 * write ahead log for multi-statement transactions.
+	 * Single-statement transactions do not encode
+	 * tsn and is_commit flag to save space.
+	 */
 	bool is_commit;
 
 	int bodycnt;
diff --git a/test/unit/xrow.cc b/test/unit/xrow.cc
index e0e7f1279b603808be68e2bb358dde95728dbb81..0a202cceaa3f925ec9fe829f948912cd6ec9fc1d 100644
--- a/test/unit/xrow.cc
+++ b/test/unit/xrow.cc
@@ -215,7 +215,7 @@ test_xrow_header_encode_decode()
 	header.lsn = 400;
 	header.tm = 123.456;
 	header.bodycnt = 0;
-	header.txn_id = header.lsn;
+	header.tsn = header.lsn;
 	header.is_commit = true;
 	uint64_t sync = 100500;
 	struct iovec vec[1];
diff --git a/test/xlog/transaction.result b/test/xlog/transaction.result
index d2d9053ae610f152fb22c5d3feee6710fce8abd8..63adb0f25ea101648d9cb893064150d271c73751 100644
--- a/test/xlog/transaction.result
+++ b/test/xlog/transaction.result
@@ -69,37 +69,37 @@ data = read_xlog(fio.pathjoin(box.cfg.wal_dir, string.rep('0', 20 - #lsn_str) ..
 ---
 ...
 -- check nothing changed for single row transactions
-data[1].HEADER.txn_id == nil and data[1].HEADER.commit == nil
+data[1].HEADER.tsn == nil and data[1].HEADER.commit == nil
 ---
 - true
 ...
-data[2].HEADER.txn_id == nil and data[2].HEADER.commit == nil
+data[2].HEADER.tsn == nil and data[2].HEADER.commit == nil
 ---
 - true
 ...
 -- check two row transaction
-data[3].HEADER.txn_id == data[3].HEADER.lsn and data[3].HEADER.commit == nil
+data[3].HEADER.tsn == data[3].HEADER.lsn and data[3].HEADER.commit == nil
 ---
 - true
 ...
-data[4].HEADER.txn_id == data[3].HEADER.txn_id and data[4].HEADER.commit == true
+data[4].HEADER.tsn == data[3].HEADER.tsn and data[4].HEADER.commit == true
 ---
 - true
 ...
 -- check four row transaction
-data[5].HEADER.txn_id == data[5].HEADER.lsn and data[5].HEADER.commit == nil
+data[5].HEADER.tsn == data[5].HEADER.lsn and data[5].HEADER.commit == nil
 ---
 - true
 ...
-data[6].HEADER.txn_id == data[5].HEADER.txn_id and data[6].HEADER.commit == nil
+data[6].HEADER.tsn == data[5].HEADER.tsn and data[6].HEADER.commit == nil
 ---
 - true
 ...
-data[7].HEADER.txn_id == data[5].HEADER.txn_id and data[7].HEADER.commit == nil
+data[7].HEADER.tsn == data[5].HEADER.tsn and data[7].HEADER.commit == nil
 ---
 - true
 ...
-data[8].HEADER.txn_id == data[5].HEADER.txn_id and data[8].HEADER.commit == true
+data[8].HEADER.tsn == data[5].HEADER.tsn and data[8].HEADER.commit == true
 ---
 - true
 ...
diff --git a/test/xlog/transaction.test.lua b/test/xlog/transaction.test.lua
index 062635098d64b1f480839a49ca97f958cc8b1e3d..2d8090b4c59c9f107c37006af465e09e49c9c95b 100644
--- a/test/xlog/transaction.test.lua
+++ b/test/xlog/transaction.test.lua
@@ -33,14 +33,14 @@ box.snapshot()
 lsn_str = tostring(lsn)
 data = read_xlog(fio.pathjoin(box.cfg.wal_dir, string.rep('0', 20 - #lsn_str) .. tostring(lsn_str) .. '.xlog'))
 -- check nothing changed for single row transactions
-data[1].HEADER.txn_id == nil and data[1].HEADER.commit == nil
-data[2].HEADER.txn_id == nil and data[2].HEADER.commit == nil
+data[1].HEADER.tsn == nil and data[1].HEADER.commit == nil
+data[2].HEADER.tsn == nil and data[2].HEADER.commit == nil
 -- check two row transaction
-data[3].HEADER.txn_id == data[3].HEADER.lsn and data[3].HEADER.commit == nil
-data[4].HEADER.txn_id == data[3].HEADER.txn_id and data[4].HEADER.commit == true
+data[3].HEADER.tsn == data[3].HEADER.lsn and data[3].HEADER.commit == nil
+data[4].HEADER.tsn == data[3].HEADER.tsn and data[4].HEADER.commit == true
 -- check four row transaction
-data[5].HEADER.txn_id == data[5].HEADER.lsn and data[5].HEADER.commit == nil
-data[6].HEADER.txn_id == data[5].HEADER.txn_id and data[6].HEADER.commit == nil
-data[7].HEADER.txn_id == data[5].HEADER.txn_id and data[7].HEADER.commit == nil
-data[8].HEADER.txn_id == data[5].HEADER.txn_id and data[8].HEADER.commit == true
+data[5].HEADER.tsn == data[5].HEADER.lsn and data[5].HEADER.commit == nil
+data[6].HEADER.tsn == data[5].HEADER.tsn and data[6].HEADER.commit == nil
+data[7].HEADER.tsn == data[5].HEADER.tsn and data[7].HEADER.commit == nil
+data[8].HEADER.tsn == data[5].HEADER.tsn and data[8].HEADER.commit == true
 box.space.test:drop()