diff --git a/src/box/lua/net_box.c b/src/box/lua/net_box.c
index 43a7d78a1122499581d490ba38e5ef380a72152b..1783da607dcc1bfb831524023e4a97ec0d165894 100644
--- a/src/box/lua/net_box.c
+++ b/src/box/lua/net_box.c
@@ -124,6 +124,15 @@ struct netbox_request {
 	/** Lua references to on_push trigger and its context. */
 	int on_push_ref;
 	int on_push_ctx_ref;
+	/**
+	 * Lua reference to a table with user-defined fields.
+	 * We allow the user to attach extra information to a future object,
+	 * e.g. a reference to a connection or the invoked method name/args.
+	 * All the information is stored in this table, which is created
+	 * lazily, on the first __newindex invocation. Until then, it's
+	 * LUA_NOREF.
+	 */
+	int index_ref;
 	/**
 	 * Lua reference to the request result or LUA_NOREF if the response
 	 * hasn't been received yet. If the response was decoded to a
@@ -167,6 +176,7 @@ netbox_request_destroy(struct netbox_request *request)
 	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, request->on_push_ref);
 	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, request->on_push_ctx_ref);
 	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, request->result_ref);
+	luaL_unref(tarantool_L, LUA_REGISTRYINDEX, request->index_ref);
 	if (request->error != NULL)
 		error_unref(request->error);
 }
@@ -1422,6 +1432,48 @@ luaT_netbox_request_gc(struct lua_State *L)
 	return 0;
 }
 
+static int
+luaT_netbox_request_index(struct lua_State *L)
+{
+	struct netbox_request *request = luaT_check_netbox_request(L, 1);
+	if (request->index_ref != LUA_NOREF) {
+		lua_rawgeti(L, LUA_REGISTRYINDEX, request->index_ref);
+		/*
+		 * Copy the key (2nd argument) to the top. Note, we don't move
+		 * it with lua_insert, like we do in __newindex, because we want
+		 * to save it for the fallback path below.
+		 */
+		lua_pushvalue(L, 2);
+		lua_rawget(L, -2);
+		if (lua_type(L, -1) != LUA_TNIL)
+			return 1;
+		/* Pop nil and the index table. */
+		lua_pop(L, 2);
+	}
+	/* Fall back on metatable methods. */
+	lua_getmetatable(L, 1);
+	/* Move the metatable before the key (2nd argument). */
+	lua_insert(L, 2);
+	lua_rawget(L, 2);
+	return 1;
+}
+
+static int
+luaT_netbox_request_newindex(struct lua_State *L)
+{
+	struct netbox_request *request = luaT_check_netbox_request(L, 1);
+	if (request->index_ref == LUA_NOREF) {
+		/* Lazily create the index table on the first invocation. */
+		lua_newtable(L);
+		request->index_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+	}
+	lua_rawgeti(L, LUA_REGISTRYINDEX, request->index_ref);
+	/* Move the index table before the key (2nd argument). */
+	lua_insert(L, 2);
+	lua_rawset(L, 2);
+	return 0;
+}
+
 /**
  * Returns true if the response was received for the given request.
  */
@@ -1666,6 +1718,7 @@ netbox_make_request(struct lua_State *L, int idx,
 		request->format = tuple_format_runtime;
 	tuple_format_ref(request->format);
 	fiber_cond_create(&request->cond);
+	request->index_ref = LUA_NOREF;
 	request->result_ref = LUA_NOREF;
 	request->error = NULL;
 	if (netbox_request_register(request, registry) != 0) {
@@ -2054,6 +2107,8 @@ luaopen_net_box(struct lua_State *L)
 
 	static const struct luaL_Reg netbox_request_meta[] = {
 		{ "__gc",           luaT_netbox_request_gc },
+		{ "__index",        luaT_netbox_request_index },
+		{ "__newindex",     luaT_netbox_request_newindex },
 		{ "is_ready",       luaT_netbox_request_is_ready },
 		{ "result",         luaT_netbox_request_result },
 		{ "wait_result",    luaT_netbox_request_wait_result },
diff --git a/test/box/net.box_fiber-async_gh-3107.result b/test/box/net.box_fiber-async_gh-3107.result
index ec2fd4f6441a030e29b7bca5e1fc475514fa89ee..ae1c2c308092b774a42a0d4b4a4bf3dec6ecbbc4 100644
--- a/test/box/net.box_fiber-async_gh-3107.result
+++ b/test/box/net.box_fiber-async_gh-3107.result
@@ -10,10 +10,7 @@ net = require('net.box')
 cond = nil
 ---
 ...
-box.schema.func.create('long_function')
----
-...
-box.schema.user.grant('guest', 'execute', 'function', 'long_function')
+box.schema.user.grant('guest', 'execute', 'universe')
 ---
 ...
 function long_function(...) cond = fiber.cond() cond:wait() return ... end
@@ -152,7 +149,109 @@ assert_no_csw(future.wait_result, future)
 ---
 - [1, 2, 3]
 ...
-box.schema.func.drop('long_function')
+-- Storing user data in future object.
+future = c:eval('return 123', {}, {is_async = true})
+---
+...
+future.abc -- nil
+---
+- null
+...
+future.abc = nil    -- delete a key that has never been set
+---
+...
+future.abc -- nil
+---
+- null
+...
+future.abc = 'abc'  -- set a value for a key
+---
+...
+future.abc -- abc
+---
+- abc
+...
+future.abc = nil    -- delete a value for a key
+---
+...
+future.abc -- nil
+---
+- null
+...
+future.abc = nil    -- delete a key with no value
+---
+...
+future.abc -- nil
+---
+- null
+...
+future:wait_result() -- 123
+---
+- [123]
+...
+-- Garbage collection of stored user data.
+future = c:eval('return 123', {}, {is_async = true})
+---
+...
+future.data1 = {1}
+---
+...
+future.data2 = {2}
+---
+...
+future.data3 = {3}
+---
+...
+gc = setmetatable({}, {__mode = 'v'})
+---
+...
+gc.data1 = future.data1
+---
+...
+gc.data2 = future.data2
+---
+...
+gc.data3 = future.data3
+---
+...
+future.data1 = nil
+---
+...
+_ = collectgarbage('collect')
+---
+...
+gc.data1 == nil
+---
+- true
+...
+future.data2 = 123
+---
+...
+_ = collectgarbage('collect')
+---
+...
+gc.data2 == nil
+---
+- true
+...
+future:wait_result() -- 123
+---
+- [123]
+...
+future = nil
+---
+...
+_ = collectgarbage('collect')
+---
+...
+_ = collectgarbage('collect')
+---
+...
+gc.data3 == nil
+---
+- true
+...
+box.schema.user.revoke('guest', 'execute', 'universe')
 ---
 ...
 c:close()
diff --git a/test/box/net.box_fiber-async_gh-3107.test.lua b/test/box/net.box_fiber-async_gh-3107.test.lua
index 71ba50b62ccbf722cab7d4df6414ed743f107f00..b6bf35ca2fa1411a09f6170c342f343c9e2edee4 100644
--- a/test/box/net.box_fiber-async_gh-3107.test.lua
+++ b/test/box/net.box_fiber-async_gh-3107.test.lua
@@ -5,8 +5,7 @@ net = require('net.box')
 -- gh-3107: fiber-async netbox.
 --
 cond = nil
-box.schema.func.create('long_function')
-box.schema.user.grant('guest', 'execute', 'function', 'long_function')
+box.schema.user.grant('guest', 'execute', 'universe')
 function long_function(...) cond = fiber.cond() cond:wait() return ... end
 function finalize_long() while not cond do fiber.sleep(0.01) end cond:signal() cond = nil end
 s = box.schema.create_space('test')
@@ -56,7 +55,41 @@ assert_no_csw(future.is_ready, future)
 assert_no_csw(future.result, future)
 assert_no_csw(future.wait_result, future)
 
-box.schema.func.drop('long_function')
+-- Storing user data in future object.
+future = c:eval('return 123', {}, {is_async = true})
+future.abc -- nil
+future.abc = nil    -- delete a key that has never been set
+future.abc -- nil
+future.abc = 'abc'  -- set a value for a key
+future.abc -- abc
+future.abc = nil    -- delete a value for a key
+future.abc -- nil
+future.abc = nil    -- delete a key with no value
+future.abc -- nil
+future:wait_result() -- 123
+
+-- Garbage collection of stored user data.
+future = c:eval('return 123', {}, {is_async = true})
+future.data1 = {1}
+future.data2 = {2}
+future.data3 = {3}
+gc = setmetatable({}, {__mode = 'v'})
+gc.data1 = future.data1
+gc.data2 = future.data2
+gc.data3 = future.data3
+future.data1 = nil
+_ = collectgarbage('collect')
+gc.data1 == nil
+future.data2 = 123
+_ = collectgarbage('collect')
+gc.data2 == nil
+future:wait_result() -- 123
+future = nil
+_ = collectgarbage('collect')
+_ = collectgarbage('collect')
+gc.data3 == nil
+
+box.schema.user.revoke('guest', 'execute', 'universe')
 
 c:close()
 s:drop()