diff --git a/extra/exports b/extra/exports
index b61f164167ab169a4e4c554763a7fed93e533b30..549f33f3e7363d0994597266628afa46b678c763 100644
--- a/extra/exports
+++ b/extra/exports
@@ -59,6 +59,7 @@ box_error_last
 box_error_message
 box_error_set
 box_error_type
+box_generate_func_id
 box_ibuf_read_range
 box_ibuf_reserve
 box_ibuf_write_range
diff --git a/src/box/box.cc b/src/box/box.cc
index 3e901de6e84c47d07bf24559488dca70cc869c4f..f15001442225a7c58e62c6c5308e2ee4e2516a13 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -30,6 +30,8 @@
  */
 #include "box/box.h"
 
+#include "func_cache.h"
+#include "schema_def.h"
 #include "trivia/config.h"
 
 #include <sys/utsname.h>
@@ -5365,15 +5367,9 @@ box_read_ffi_enable(void)
 		box_read_ffi_is_disabled = false;
 }
 
-int
-box_generate_space_id(uint32_t *new_space_id, bool is_temporary)
+static int
+next_u32_id(uint32_t space_id, uint32_t id_range_end, uint32_t *max_id)
 {
-	assert(new_space_id != NULL);
-	uint32_t id_range_begin = !is_temporary ?
-		BOX_SYSTEM_ID_MAX + 1 : BOX_SPACE_ID_TEMPORARY_MIN;
-	uint32_t id_range_end = !is_temporary ?
-		(uint32_t)BOX_SPACE_ID_TEMPORARY_MIN :
-		(uint32_t)BOX_SPACE_MAX + 1;
 	char key_buf[16];
 	char *key_end = key_buf;
 	key_end = mp_encode_array(key_end, 1);
@@ -5383,7 +5379,7 @@ box_generate_space_id(uint32_t *new_space_id, bool is_temporary)
 	auto guard = make_scoped_guard([=] {
 		fiber_set_user(fiber(), orig_credentials);
 	});
-	box_iterator_t *it = box_index_iterator(BOX_SPACE_ID, 0, ITER_LT,
+	box_iterator_t *it = box_index_iterator(space_id, 0, ITER_LT,
 						key_buf, key_end);
 	if (it == NULL)
 		return -1;
@@ -5393,9 +5389,25 @@ box_generate_space_id(uint32_t *new_space_id, bool is_temporary)
 	if (rc != 0)
 		return -1;
 	assert(res != NULL);
-	uint32_t max_id = 0;
-	rc = tuple_field_u32(res, 0, &max_id);
+	rc = tuple_field_u32(res, 0, max_id);
 	assert(rc == 0);
+	return 0;
+}
+
+int
+box_generate_space_id(uint32_t *new_space_id, bool is_temporary)
+{
+	if (box_check_configured() < 0)
+		return -1;
+	assert(new_space_id != NULL);
+	uint32_t id_range_begin = !is_temporary ?
+		BOX_SYSTEM_ID_MAX + 1 : BOX_SPACE_ID_TEMPORARY_MIN;
+	uint32_t id_range_end = !is_temporary ?
+		(uint32_t)BOX_SPACE_ID_TEMPORARY_MIN :
+		(uint32_t)BOX_SPACE_MAX + 1;
+	uint32_t max_id = 0;
+	if (next_u32_id(BOX_SPACE_ID, id_range_end, &max_id) != 0)
+		return -1;
 	if (max_id < id_range_begin)
 		max_id = id_range_begin - 1;
 	*new_space_id = space_cache_find_next_unused_id(max_id);
@@ -5415,6 +5427,35 @@ box_generate_space_id(uint32_t *new_space_id, bool is_temporary)
 	return 0;
 }
 
+API_EXPORT int
+box_generate_func_id(uint32_t *new_func_id, bool use_reserved_range)
+{
+	if (box_check_configured() < 0)
+		return -1;
+	uint32_t id_range_begin = use_reserved_range ?
+		BOX_FUNCTION_RESERVED_MIN : 1;
+	uint32_t id_range_end = use_reserved_range ?
+		(uint32_t)BOX_FUNCTION_MAX + 1 :
+		(uint32_t)BOX_FUNCTION_RESERVED_MIN;
+	uint32_t max_id = 0;
+	if (next_u32_id(BOX_FUNC_ID, id_range_end, &max_id) != 0)
+		return -1;
+	if (max_id < id_range_begin)
+		max_id = id_range_begin - 1;
+	*new_func_id = func_cache_find_next_unused_id(id_range_begin - 1);
+	/* Try again if overflowed. */
+	if (*new_func_id >= id_range_end) {
+		*new_func_id =
+			func_cache_find_next_unused_id(id_range_begin - 1);
+		/* Second overflow means all ids are occupied. */
+		if (*new_func_id >= id_range_end) {
+			diag_set(ClientError, ER_CANT_GENERATE, "function id");
+			return -1;
+		}
+	}
+	return 0;
+}
+
 static void
 on_garbage_collection(void)
 {
diff --git a/src/box/box.h b/src/box/box.h
index 216dc89319682675690b4595eb55b7af432b862f..b1b115acd10eadf6d9508c93828978e7478fd136 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -834,6 +834,10 @@ boxk(int type, uint32_t space_id, const char *format, ...);
 int
 box_generate_space_id(uint32_t *new_space_id, bool is_temporary);
 
+/** Generate unique id for a function. */
+API_EXPORT int
+box_generate_func_id(uint32_t *new_func_id, bool use_reserved_range);
+
 /**
  * Broadcast the identification of the instance
  */
diff --git a/src/box/errcode.h b/src/box/errcode.h
index 52cbbd7284448327325b2d5d91da66b24b607262..1a6015a32414f275d69563ed8b9670a5e1812b26 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -322,7 +322,7 @@ struct errcode_record {
 	/*267 */_(ER_EXCEEDED_VDBE_MAX_STEPS,	"Reached a limit on max executed vdbe opcodes. Limit: %u") \
 	/*268 */_(ER_ILLEGAL_OPTIONS,		"Illegal options: %s")   \
 	/*269 */_(ER_ILLEGAL_OPTIONS_FORMAT,    "Each option in third argument must be a table containing only one key value pair")   \
-	/*270 */_(ER_UNUSED4,			"") \
+	/*270 */_(ER_CANT_GENERATE,		"Can't generate %s") \
 	/*271 */_(ER_UNUSED5,			"") \
 	/*272 */_(ER_SCHEMA_UPGRADE_IN_PROGRESS, "Schema upgrade is already in progress") \
 	/*273 */_(ER_UNUSED7,			"") \
diff --git a/src/box/func_cache.c b/src/box/func_cache.c
index 1a465642d10e58e81196fec0968df9a025efd0f8..4e48f34c34fad7fb42c2ce275bcf97bc7c19a9b1 100644
--- a/src/box/func_cache.c
+++ b/src/box/func_cache.c
@@ -7,6 +7,7 @@
 
 #include <assert.h>
 #include "assoc.h"
+#include "schema_def.h"
 
 /** ID -> func dictionary. */
 static struct mh_i32ptr_t *funcs;
@@ -87,6 +88,16 @@ func_by_name(const char *name, uint32_t name_len)
 	return (struct func *)mh_strnptr_node(funcs_by_name, func)->val;
 }
 
+uint32_t
+func_cache_find_next_unused_id(uint32_t cur_id)
+{
+	for (cur_id++; cur_id <= BOX_FUNCTION_MAX; cur_id++) {
+		if (func_by_id(cur_id) == NULL)
+			return cur_id;
+	}
+	return cur_id;
+}
+
 void
 func_pin(struct func *func, struct func_cache_holder *holder,
 	 enum func_holder_type type)
diff --git a/src/box/func_cache.h b/src/box/func_cache.h
index b95138165e38d840b57da3c9901470426716c73c..1c4a3bc28f45cd7060a9ae05e60b3b38956f4d1d 100644
--- a/src/box/func_cache.h
+++ b/src/box/func_cache.h
@@ -90,6 +90,13 @@ func_by_id(uint32_t fid);
 struct func *
 func_by_name(const char *name, uint32_t name_len);
 
+/**
+ * Find minimal unused id, which is greater than cur_id.
+ * If there is no available id, BOX_FUNCTION_MAX + 1 is returned.
+ */
+uint32_t
+func_cache_find_next_unused_id(uint32_t cur_id);
+
 /**
  * Register that there is a @a holder of type @a type that is dependent
  * on function @a func.
diff --git a/src/box/lua/misc.cc b/src/box/lua/misc.cc
index 3092d03aea47887918cbe407bf05552e9d71744a..377f77f416498c98b42d1f9f5d2cfdf8aeb6aa3b 100644
--- a/src/box/lua/misc.cc
+++ b/src/box/lua/misc.cc
@@ -235,6 +235,20 @@ lbox_generate_space_id(lua_State *L)
 	return 1;
 }
 
+/** Generate unique id for a function. */
+static int
+lbox_generate_func_id(lua_State *L)
+{
+	assert(lua_gettop(L) >= 1);
+	assert(lua_isboolean(L, 1) == 1);
+	bool use_reserved_range = lua_toboolean(L, 1) != 0;
+	uint32_t ret = 0;
+	if (box_generate_func_id(&ret, use_reserved_range) != 0)
+		return luaT_error(L);
+	lua_pushnumber(L, ret);
+	return 1;
+}
+
 /* }}} */
 
 /** {{{ Helper that generates user auth data. **/
@@ -541,6 +555,7 @@ box_lua_misc_init(struct lua_State *L)
 		{"read_view_list", lbox_read_view_list},
 		{"read_view_status", lbox_read_view_status},
 		{"generate_space_id", lbox_generate_space_id},
+		{"generate_func_id", lbox_generate_func_id},
 		{NULL, NULL}
 	};
 
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 4c8f0f502656fdee4601917dc2f09755455ce54b..e057cc36c728c0ee1564074f052d4a715db5a682 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -3074,7 +3074,8 @@ end
 box.schema.func = {}
 box.schema.func.create = function(name, opts)
     opts = opts or {}
-    check_param_table(opts, { setuid = 'boolean',
+    check_param_table(opts, { id = 'number',
+                              setuid = 'boolean',
                               if_not_exists = 'boolean',
                               language = 'string', body = 'string',
                               is_deterministic = 'boolean',
@@ -3108,12 +3109,18 @@ box.schema.func.create = function(name, opts)
     if opts.takes_raw_args then
         opts.opts.takes_raw_args = opts.takes_raw_args
     end
-    _func:auto_increment{session.euid(), name, opts.setuid, opts.language,
-                         opts.body, opts.routine_type, opts.param_list,
-                         opts.returns, opts.aggregate, opts.sql_data_access,
-                         opts.is_deterministic, opts.is_sandboxed,
-                         opts.is_null_call, opts.exports, opts.opts,
-                         opts.comment, opts.created, opts.last_altered}
+    if opts.id == nil then
+        opts.id = internal.generate_func_id(false)
+        if opts.id == nil then
+            box.error()
+        end
+    end
+    _func:insert{opts.id, session.euid(), name, opts.setuid, opts.language,
+                  opts.body, opts.routine_type, opts.param_list,
+                  opts.returns, opts.aggregate, opts.sql_data_access,
+                  opts.is_deterministic, opts.is_sandboxed,
+                  opts.is_null_call, opts.exports, opts.opts,
+                  opts.comment, opts.created, opts.last_altered}
 end
 
 box.schema.func.drop = function(name, opts)
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 8380701bc08c69b3961274e45c668f414050171f..5f964cb4f8c8944191782bdf71dd3b426d478f0c 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -40,7 +40,7 @@ extern "C" {
 enum {
 	BOX_ENGINE_MAX = 3, /* + 1 to the actual number of engines */
 	BOX_SPACE_MAX = INT32_MAX,
-	BOX_FUNCTION_MAX = 32000,
+	BOX_FUNCTION_MAX = INT32_MAX,
 	BOX_INDEX_MAX = 128,
 	BOX_NAME_MAX = 65000,
 	BOX_INVALID_NAME_MAX = 64,
@@ -65,6 +65,13 @@ enum {
 	 * choose an id from outside this range.
 	 */
 	BOX_SPACE_ID_TEMPORARY_MIN = (1 << 30),
+	/**
+	 * Start of the range of reserved function ids. By default functions get
+	 * ids from the default range (smaller than BOX_FUNCTION_RESERVED_MIN),
+	 * but the user is free to choose an id from the reserved range
+	 * explicitly.
+	 */
+	BOX_FUNCTION_RESERVED_MIN = 32001,
 };
 static_assert(BOX_INVALID_NAME_MAX <= BOX_NAME_MAX,
 	      "invalid name max is less than name max");
diff --git a/test/box/access.result b/test/box/access.result
index fc4f7478bd0685a4edb46c9380948ca20185d3d3..87bcd1f6630a7a824cc48b0dd95aceacdba8dc69 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -1302,7 +1302,7 @@ box.schema.user.create('test')
 ...
 box.schema.func.create('test')
 ---
-- error: Read access to space '_func' is denied for user 'guest'
+- error: Write access to space '_func' is denied for user 'guest'
 ...
 box.session.su('admin')
 ---
@@ -1508,7 +1508,7 @@ box.schema.user.create('test_user')
 ...
 box.schema.func.create('test_func')
 ---
-- error: Read access to space '_func' is denied for user 'tester'
+- error: Write access to space '_func' is denied for user 'tester'
 ...
 box.session.su("admin")
 ---
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index be2464104f95db6613121d802b2d4f1972198f1f..69dd2702cc4d7ce44406c627bad9a421627e558c 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -181,7 +181,7 @@ gs = box.schema.space.create('guest_space')
 --
 box.schema.func.create('guest_func')
 ---
-- error: Read access to space '_func' is denied for user 'guest'
+- error: Write access to space '_func' is denied for user 'guest'
 ...
 session.su('admin', box.schema.user.grant, "guest", "read", "universe")
 ---
diff --git a/test/box/error.result b/test/box/error.result
index 9c94062bdd7e2b571763d0d04b223ce304508871..fc54b7901b8ec83c46ca348e8cde5a4bb8429e24 100644
--- a/test/box/error.result
+++ b/test/box/error.result
@@ -488,6 +488,7 @@ t;
  |   267: box.error.EXCEEDED_VDBE_MAX_STEPS
  |   268: box.error.ILLEGAL_OPTIONS
  |   269: box.error.ILLEGAL_OPTIONS_FORMAT
+ |   270: box.error.CANT_GENERATE
  |   272: box.error.SCHEMA_UPGRADE_IN_PROGRESS
  |   274: box.error.UNCONFIGURED
  | ...
diff --git a/test/box/function1.result b/test/box/function1.result
index eef8c6fe1a545443e1e32380e223a4104fa7a68d..10275dba79dab2a9ca4a42e109013c014b50c2fd 100644
--- a/test/box/function1.result
+++ b/test/box/function1.result
@@ -97,7 +97,7 @@ box.func["function1.args"]
   exports:
     lua: true
     sql: false
-  id: 66
+  id: 2
   takes_raw_args: false
   setuid: false
   is_multikey: false
@@ -589,7 +589,7 @@ func
   exports:
     lua: true
     sql: false
-  id: 66
+  id: 2
   takes_raw_args: false
   setuid: false
   is_multikey: false
@@ -662,7 +662,7 @@ func
   exports:
     lua: true
     sql: false
-  id: 66
+  id: 2
   takes_raw_args: false
   setuid: false
   is_multikey: false
diff --git a/test/sql-luatest/func_id_test.lua b/test/sql-luatest/func_id_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..c66beab21e9292584fa8b0dd717a4bf80cac95c5
--- /dev/null
+++ b/test/sql-luatest/func_id_test.lua
@@ -0,0 +1,73 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all(function()
+    g.server = server:new({alias = 'func_id'})
+    g.server:start()
+end)
+
+g.after_all(function()
+    g.server:stop()
+end)
+
+g.test_func_from_reserved_range = function()
+    g.server:exec(function()
+        local id = box.internal.generate_func_id(true)
+        t.assert(id > 32000)
+        local def = {language = 'LUA',
+                     body = 'function() return 1 end',
+                     id = id}
+        local _, err = box.schema.func.create('abc', def)
+        t.assert(err == nil)
+        local next_id = box.internal.generate_func_id(true)
+        t.assert(next_id == id + 1)
+        _, err = box.schema.func.drop(id)
+        t.assert(err == nil)
+    end)
+end
+
+g.test_func_from_default_range = function()
+    g.server:exec(function()
+        local id = box.internal.generate_func_id(false)
+        t.assert(id <= 32000)
+        local def = {language = 'LUA', body = 'function() return 1 end' }
+        local _, err = box.schema.func.create('abc', def)
+        t.assert(err == nil)
+        local next_id = box.internal.generate_func_id(false)
+        t.assert(next_id == id + 1)
+        _, err = box.schema.func.drop(id)
+        t.assert(err == nil)
+    end)
+end
+
+g.test_ffi_reserved_range = function()
+    g.server:exec(function()
+        local id = box.internal.generate_func_id(true)
+        local ffi = require('ffi')
+        ffi.cdef([[int box_generate_func_id(
+            uint32_t *new_func_id,
+            bool use_reserved_range
+        );]])
+        local ptr = ffi.new('uint32_t[1]')
+        local res = ffi.C.box_generate_func_id(ptr, true)
+        t.assert(res == 0)
+        t.assert(ptr[0] == id)
+    end)
+end
+
+g.test_ffi_default_range = function()
+    g.server:exec(function()
+        local id = box.internal.generate_func_id(false)
+        local ffi = require('ffi')
+        ffi.cdef([[int box_generate_func_id(
+            uint32_t *new_func_id,
+            bool use_reserved_range
+        );]])
+        local ptr = ffi.new('uint32_t[1]')
+        local res = ffi.C.box_generate_func_id(ptr, false)
+        t.assert(res == 0)
+        t.assert(ptr[0] == id)
+    end)
+end
diff --git a/test/wal_off/func_max.result b/test/wal_off/func_max.result
index a3ab5b431c99d32cbc9101be87d2575b23c800fd..bc0abb55196b386032d20744a40766b11c9b995a 100644
--- a/test/wal_off/func_max.result
+++ b/test/wal_off/func_max.result
@@ -42,11 +42,11 @@ test_run:cmd("setopt delimiter ''");
 ...
 func_limit()
 ---
-- error: 'Failed to create function ''func31936'': function id is too big'
+- error: Can't generate function id
 ...
 drop_limit_func()
 ---
-- error: Function 'func31936' does not exist
+- error: Function 'func31999' does not exist
 ...
 box.schema.user.create('testuser')
 ---
@@ -62,11 +62,11 @@ session.su('testuser')
 ...
 func_limit()
 ---
-- error: 'Failed to create function ''func31936'': function id is too big'
+- error: Can't generate function id
 ...
 drop_limit_func()
 ---
-- error: Function 'func31936' does not exist
+- error: Function 'func31999' does not exist
 ...
 session.su('admin')
 ---