diff --git a/doc/user/stored-procedures.xml b/doc/user/stored-procedures.xml
index b2976287c3f95fce3aba5d3efda37fb7bc79bd96..40143a9ee9fa2ef8bf8c772898d71385b8c856fc 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 60719cbc17848cefa169c1857a3918c7c567019e..3695eb049220eb2f0ba0c5a00a04a2fe39243389 100644
--- a/include/session.h
+++ b/include/session.h
@@ -93,3 +93,6 @@ session_init();
 
 void
 session_free();
+
+void
+session_storage_cleanup(int sid);
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8cbbb0b56d62f89bcd0cb5cd05df32d9b5f6e20a..a98372c62609aa17dd2cb12d603a139f4d42d929 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -21,6 +21,7 @@ endif()
 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)
 set(bin_sources)
 bin_source(bin_sources bootstrap.snap bootstrap.h)
 
diff --git a/src/lua/init.cc b/src/lua/init.cc
index 66906f35dc6c3c0428b291fe8f25ae5c7405e23b..3fda445bc7d82b614f675cd786869ddeacfbd558 100644
--- a/src/lua/init.cc
+++ b/src/lua/init.cc
@@ -80,7 +80,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 };
 
 /*
  * {{{ box Lua library: common functions
diff --git a/src/lua/session.cc b/src/lua/session.cc
index 1036ce04a87be08dfeeee3197b936db249953ef6..57c5dfacf5c0eb4547526d93ad8b117c3c6bf538 100644
--- a/src/lua/session.cc
+++ b/src/lua/session.cc
@@ -117,6 +117,28 @@ lbox_session_on_disconnect(struct lua_State *L)
 				  lbox_session_run_trigger);
 }
 
+void
+session_storage_cleanup(int sid)
+{
+	static int ref = LUA_REFNIL;
+	struct lua_State *L = tarantool_L;
+
+	int top = lua_gettop(L);
+
+	if (ref == LUA_REFNIL) {
+		lua_getfield(L, LUA_GLOBALSINDEX, "box");
+		lua_getfield(L, -1, "session");
+		lua_getmetatable(L, -1);
+		lua_getfield(L, -1, "aggregate_storage");
+		ref = luaL_ref(L, LUA_REGISTRYINDEX);
+	}
+	lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
+
+	lua_pushnil(L);
+	lua_rawseti(L, -2, sid);
+	lua_settop(L, top);
+}
+
 void
 tarantool_lua_session_init(struct lua_State *L)
 {
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 e320563a521d48f338572d5f4c5609656ca9863d..6973db23132cc9f1a5acffe7bb0ec141b21cd453 100644
--- a/src/session.cc
+++ b/src/session.cc
@@ -87,6 +87,7 @@ session_destroy(uint32_t sid)
 	} catch (...) {
 		/* 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)"