diff --git a/doc/user/stored-procedures.xml b/doc/user/stored-procedures.xml
index ab8d735ca210026600449a6fe5a1b9cf4bca9c49..cbc988fa95b285778b9b6700c8c018194f914c8c 100644
--- a/doc/user/stored-procedures.xml
+++ b/doc/user/stored-procedures.xml
@@ -2149,6 +2149,16 @@ event.
         is not connected.</simpara></listitem>
     </varlistentry>
 
+    <varlistentry>
+        <term>
+           <emphasis role="lua">box.session.storage</emphasis>
+        </term>
+        <listitem><simpara>A virtual table that is local for each session.
+        It can be used to store session-specific values. The lifetime is the
+        same as the session's (until disconnect).
+        </simpara></listitem>
+    </varlistentry>
+
 </variablelist>
     <para>
 This module also makes it possible to define triggers on connect
diff --git a/include/session.h b/include/session.h
index 37c5799c410a77342979cd48cb205868ab0baba8..a8e94cd35b23e3bb79243c6d4a555d99d28945d4 100644
--- a/include/session.h
+++ b/include/session.h
@@ -104,3 +104,6 @@ session_init();
 
 void
 session_free();
+
+void
+session_storage_cleanup(int sid);
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5b1c98c9cd2e480709343bbc4fe244819329c964..f96545ee8cce465beb663cf1f17288b43ea7331a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -62,6 +62,7 @@ set_property(DIRECTORY PROPERTY CLEAN_NO_CUSTOM true)
 file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/src/lua)
 set(lua_sources)
 lua_source(lua_sources lua/uuid.lua)
+lua_source(lua_sources lua/session.lua)
 
 add_custom_target(generate_lua_sources
     WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src/box
diff --git a/src/box/box_lua.cc b/src/box/box_lua.cc
index e047033b5fcd184c3e43c59c2f76739fb839e113..509f44091f6e9a0252d3277f0ea8e282e7e2ebe5 100644
--- a/src/box/box_lua.cc
+++ b/src/box/box_lua.cc
@@ -62,7 +62,7 @@ static const char *lua_sources[] = { box_lua, box_net_lua, misc_lua, sql_lua, NU
  * Lua coroutines (lua_newthread()) to have multiple
  * procedures running at the same time.
  */
-static lua_State *root_L;
+lua_State *root_L;
 
 /*
  * Functions, exported in box_lua.h should have prefix
diff --git a/src/lua/init.cc b/src/lua/init.cc
index 8fa810a7618afc8a908ca3878298ba12127ca957..ed78c07fcf4a5c6b718a1e72b0ebff26070a68bd 100644
--- a/src/lua/init.cc
+++ b/src/lua/init.cc
@@ -78,7 +78,8 @@ struct lua_State *tarantool_L;
 
 /* contents of src/lua/ files */
 extern char uuid_lua[];
-static const char *lua_sources[] = { uuid_lua, NULL };
+extern char session_lua[];
+static const char *lua_sources[] = { uuid_lua, session_lua, NULL };
 
 /**
  * Remember the output of the administrative console in the
diff --git a/src/lua/session.cc b/src/lua/session.cc
index 508c34247cbd689a78bc0d9fd9113d94090e57fe..05a21ce4baa85314de40c59b3cafe0b3ca6c79c8 100644
--- a/src/lua/session.cc
+++ b/src/lua/session.cc
@@ -40,6 +40,7 @@ extern "C" {
 #include "sio.h"
 
 static const char *sessionlib_name = "box.session";
+extern lua_State *root_L;
 
 /**
  * Return a unique monotonic session
@@ -175,10 +176,26 @@ lbox_session_on_disconnect(struct lua_State *L)
 	return lbox_session_set_trigger(L, &on_disconnect);
 }
 
-static const struct luaL_reg lbox_session_meta [] = {
-	{"id", lbox_session_id},
-	{NULL, NULL}
-};
+void
+session_storage_cleanup(int sid)
+{
+	static int ref = LUA_REFNIL;
+
+	int top = lua_gettop(root_L);
+
+	if (ref == LUA_REFNIL) {
+		lua_getfield(root_L, LUA_GLOBALSINDEX, "box");
+		lua_getfield(root_L, -1, "session");
+		lua_getmetatable(root_L, -1);
+		lua_getfield(root_L, -1, "aggregate_storage");
+		ref = luaL_ref(root_L, LUA_REGISTRYINDEX);
+	}
+	lua_rawgeti(root_L, LUA_REGISTRYINDEX, ref);
+
+	lua_pushnil(root_L);
+	lua_rawseti(root_L, -2, sid);
+	lua_settop(root_L, top);
+}
 
 static const struct luaL_reg sessionlib[] = {
 	{"id", lbox_session_id},
diff --git a/src/lua/session.lua b/src/lua/session.lua
new file mode 100644
index 0000000000000000000000000000000000000000..e3e4bd7cd7c41d04180607764ed84ec94479528e
--- /dev/null
+++ b/src/lua/session.lua
@@ -0,0 +1,21 @@
+-- box.session.lua
+
+setmetatable(box.session, {
+    __index = function(tbl, idx)
+
+        if idx ~= 'storage' then
+            return
+        end
+
+        local sid = box.session.id()
+
+        local mt = getmetatable(tbl)
+
+        if mt.aggregate_storage[ sid ] == nil then
+            mt.aggregate_storage[ sid ] = {}
+        end
+        return mt.aggregate_storage[ sid ]
+    end,
+
+    aggregate_storage = {}
+})
diff --git a/src/session.cc b/src/session.cc
index eeb9c524eab9798e985352bf4eba031f537c0305..dcddeb38221a03dd81ef3ab02ca5ca46de63fc55 100644
--- a/src/session.cc
+++ b/src/session.cc
@@ -93,6 +93,7 @@ session_destroy(uint32_t sid)
 			/* catch all. */
 		}
 	}
+	session_storage_cleanup(sid);
 	struct mh_i32ptr_node_t node = { sid, NULL };
 	mh_i32ptr_remove(session_registry, &node, NULL);
 }
diff --git a/test/box/session.storage.result b/test/box/session.storage.result
new file mode 100644
index 0000000000000000000000000000000000000000..1e36006a2c1d471f33ccf9449b88392c414f007f
--- /dev/null
+++ b/test/box/session.storage.result
@@ -0,0 +1,81 @@
+lua dump = function(data) return "'" .. box.cjson.encode(data) .. "'" end
+---
+...
+lua type(box.session.id())
+---
+ - number
+...
+lua box.session.unknown_field
+---
+ - nil
+...
+lua type(box.session.storage)
+---
+ - table
+...
+lua box.session.storage.abc = 'cde'
+---
+...
+lua box.session.storage.abc
+---
+ - cde
+...
+lua all = getmetatable(box.session).aggregate_storage
+---
+...
+lua type(box.session.storage)
+---
+ - table
+...
+lua type(box.session.storage.abc)
+---
+ - nil
+...
+lua box.session.storage.abc = 'def'
+---
+...
+lua box.session.storage.abc
+---
+ - def
+...
+lua box.session.storage.abc
+---
+ - cde
+...
+lua dump(all[box.session.id()])
+---
+ - '{"abc":"def"}'
+...
+lua dump(all[box.session.id()])
+---
+ - '{"abc":"cde"}'
+...
+lua tres1 = {}
+---
+...
+lua tres2 = {}
+---
+...
+lua for k,v in pairs(all) do table.insert(tres1, v.abc) end
+---
+...
+lua box.fiber.sleep(.01)
+---
+...
+lua for k,v in pairs(all) do table.insert(tres2, v.abc) end
+---
+...
+lua table.sort(tres1)
+---
+...
+lua table.sort(tres2)
+---
+...
+lua dump(tres1)
+---
+ - '["cde","def"]'
+...
+lua dump(tres2)
+---
+ - '["cde"]'
+...
diff --git a/test/box/session.storage.test b/test/box/session.storage.test
new file mode 100644
index 0000000000000000000000000000000000000000..da6c785084516b7d835af3a268c089a45d40ad6c
--- /dev/null
+++ b/test/box/session.storage.test
@@ -0,0 +1,35 @@
+# encoding: tarantool
+
+from lib.admin_connection import AdminConnection
+from lib.box_connection import BoxConnection
+
+exec admin "lua dump = function(data) return \"'\" .. box.cjson.encode(data) .. \"'\" end"
+
+exec admin "lua type(box.session.id())"
+exec admin "lua box.session.unknown_field"
+exec admin "lua type(box.session.storage)"
+exec admin "lua box.session.storage.abc = 'cde'"
+exec admin "lua box.session.storage.abc"
+
+exec admin "lua all = getmetatable(box.session).aggregate_storage"
+
+con1 = AdminConnection('localhost', server.admin_port)
+exec con1  "lua type(box.session.storage)"
+exec con1  "lua type(box.session.storage.abc)"
+exec con1  "lua box.session.storage.abc = 'def'"
+exec con1  "lua box.session.storage.abc"
+exec admin "lua box.session.storage.abc"
+exec con1  "lua dump(all[box.session.id()])"
+exec admin "lua dump(all[box.session.id()])"
+exec admin "lua tres1 = {}"
+exec admin "lua tres2 = {}"
+exec admin "lua for k,v in pairs(all) do table.insert(tres1, v.abc) end"
+con1.disconnect()
+# to call session cleanup
+exec admin "lua box.fiber.sleep(.01)"
+exec admin "lua for k,v in pairs(all) do table.insert(tres2, v.abc) end"
+
+exec admin "lua table.sort(tres1)"
+exec admin "lua table.sort(tres2)"
+exec admin "lua dump(tres1)"
+exec admin "lua dump(tres2)"