diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a10327c23d82b07d22de7b9f501594f0d5738be3..3f04eebbccac119cc00d817882d75ee085df367b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -104,6 +104,7 @@ set (server_sources
      backtrace.cc
      proc_title.c
      coeio_file.c
+     lua/console.c
      lua/digest.c
      lua/init.c
      lua/fiber.c
diff --git a/src/lua/console.c b/src/lua/console.c
new file mode 100644
index 0000000000000000000000000000000000000000..560394404d898b097e7d14c144873b6328a2a148
--- /dev/null
+++ b/src/lua/console.c
@@ -0,0 +1,580 @@
+/*
+ * Copyright 2010-2016, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "lua/console.h"
+#include "lua/utils.h"
+#include "fiber.h"
+#include "coio.h"
+#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+/*
+ * Completion engine (Mike Paul's).
+ * Used internally when collecting completions locally. Also a Lua
+ * wrapper is provided enabling a remote server to compute completions
+ * for a client.
+ */
+static char **
+lua_rl_complete(lua_State *L, const char *text, int start, int end);
+
+/*
+ * Lua state that made the pending readline call.
+ * This Lua state is accessed in readline callbacks. Unfortunately
+ * readline library doesn't allow to pass it as a function argument.
+ * Two concurrent readline() calls never happen.
+ */
+static struct lua_State *readline_L;
+
+/*
+ * console_completion_handler()
+ * Called by readline to collect plausible completions;
+ * The call stack is as follows:
+ *
+ * - lbox_console_readline
+ *  - (loop) rl_callback_read_char
+ *    - console_completion_handler
+ *
+ * Delegates to the func selected when the call to lbox_console_readline
+ * was made, e.g. readline({ completion = ... }).
+ */
+static char **
+console_completion_handler(const char *text, int start, int end)
+{
+	size_t n, i;
+	char **res;
+
+	/*
+	 * The lbox_console_readline() frame is still on the top of Lua
+	 * stack. We can reach the function arguments. Assuming arg#1 is
+	 * the options table.
+	 */
+	lua_getfield(readline_L, 1, "completion");
+	if (lua_isnil(readline_L, -1)) {
+		lua_pop(readline_L, 1);
+		return NULL;
+	}
+
+	/*
+	 * If the completion func is lbox_console_completion_handler()
+	 * /we have it in upvalue #1/ which is a wrapper on top of
+	 * lua_rl_complete, call lua_rl_complete func directly.
+	 */
+	if (lua_equal(readline_L, -1, lua_upvalueindex(1))) {
+		lua_pop(readline_L, 1);
+		return lua_rl_complete(readline_L, text, start, end);
+	}
+
+	/* Slow path - arbitrary completion handler. */
+	lua_pushstring(readline_L, text);
+	lua_pushinteger(readline_L, start);
+	lua_pushinteger(readline_L, end);
+	if (lua_pcall(readline_L, 3, 1, 0) != 0 ||
+	    !lua_istable(readline_L, -1)) {
+
+		lua_pop(readline_L, 1);
+		return NULL;
+	}
+	n = lua_objlen(readline_L, -1);
+	res = malloc(sizeof(res[0]) * (n + 1));
+	if (res == NULL) {
+		lua_pop(readline_L, 1);
+		return NULL;
+	}
+	res[n] = NULL;
+	for (i = 0; i < n; i++) {
+		lua_pushinteger(readline_L, i + 1);
+		lua_gettable(readline_L, -2);
+		res[i] = strdup(lua_tostring(readline_L, -1));
+		lua_pop(readline_L, 1);
+	}
+	lua_pop(readline_L, 1);
+	return res;
+}
+
+/*
+ * console_push_line()
+ * Readline invokes this callback once the whole line is ready.
+ * The call stack is as follows:
+ *
+ * - lbox_console_readline
+ *  - (loop) rl_callback_read_char
+ *    - console_push_line
+ *
+ * The callback creates a copy of the line on the Lua stack; this copy
+ * becomes the lbox_console_readline()'s ultimate result.
+ */
+static void
+console_push_line(char *line)
+{
+	/* XXX pushnil/pushstring may err */
+	if (line == NULL)
+		lua_pushnil(readline_L);
+	else
+		lua_pushstring(readline_L, line);
+
+#ifdef HAVE_GNU_READLINE
+	/*
+	 * This is to avoid a stray prompt on the next line with GNU
+	 * readline. Interestingly, it botches the terminal when
+	 * attempted with libeditline.
+	 */
+	rl_callback_handler_install(NULL, NULL);
+#endif
+}
+
+/* implements readline() Lua API */
+static int
+lbox_console_readline(struct lua_State *L)
+{
+	const char *prompt = NULL;
+	int top;
+
+	rl_attempted_completion_function = NULL;
+
+	if (lua_gettop(L) > 0) {
+		switch (lua_type(L, 1)) {
+		case LUA_TSTRING:
+			prompt = lua_tostring(L, 1);
+			break;
+		case LUA_TTABLE:
+			lua_getfield(L, 1, "prompt");
+			prompt = lua_tostring(L, -1);
+			lua_pop(L, 1);
+			/* the handler assumes arg #1 is a table */
+			rl_attempted_completion_function =
+				console_completion_handler;
+			break;
+		default:
+			luaL_error(L, "readline([prompt])");
+		}
+	}
+
+	if (prompt == NULL)
+		prompt = "> ";
+
+	if (readline_L != NULL)
+		luaL_error(L, "readline(): earlier call didn't complete yet");
+
+	readline_L = L;
+	rl_completer_word_break_characters =
+		"\t\r\n !\"#$%&'()*+,-/;<=>?@[\\]^`{|}~";
+	rl_completer_quote_characters = "\"'";
+#if RL_READLINE_VERSION < 0x0600
+	rl_completion_append_character = '\0';
+#endif
+	/*
+	 * Readline library provides eventloop-friendly API; repeat
+	 * until console_push_line() manages to capture the result.
+	 */
+	rl_callback_handler_install(prompt, console_push_line);
+	top = lua_gettop(L);
+	while (top == lua_gettop(L) &&
+	       coio_wait(STDIN_FILENO, COIO_READ, TIMEOUT_INFINITY)) {
+
+		rl_callback_read_char();
+	}
+
+	readline_L = NULL;
+	/* Incidents happen. */
+#pragma GCC poison readline_L
+	rl_attempted_completion_function = NULL;
+	luaL_testcancel(L);
+	return 1;
+}
+
+/* C string array to lua table converter */
+static int
+console_completion_helper(struct lua_State *L)
+{
+	size_t i;
+	char **res = *(char ***)lua_topointer(L, -1);
+	assert(lua_islightuserdata(L, -1));
+	assert(L != NULL);
+	lua_createtable(L, 0, 0);
+	for (i = 0; res[i]; i++) {
+		lua_pushstring(L, res[i]);
+		lua_rawseti(L, -2, i + i);
+	}
+	return 1;
+}
+
+/*
+ * completion_handler() Lua API
+ * Exposing completion engine to Lua.
+ */
+static int
+lbox_console_completion_handler(struct lua_State *L)
+{
+	size_t i;
+	char **res;
+	int st;
+
+	/*
+	 * Prepare for the future pcall;
+	 * this may err, hence do it before res is created
+	 */
+	lua_pushcfunction(L, console_completion_helper);
+	lua_pushlightuserdata(L, &res);
+
+	res = lua_rl_complete(L, lua_tostring(L, 1),
+			      lua_tointeger(L, 2), lua_tointeger(L, 3));
+
+	if (res == NULL) {
+		return 0;
+	}
+
+	st = lua_pcall(L, 1, 1, 0);
+
+	/* free res */
+	for (i = 0; res[i]; i++) {
+		free(res[i]);
+	}
+	free(res);
+	res = NULL;
+
+	if (st != 0) {
+		lua_error(L);
+	}
+
+	return 1;
+}
+
+static int
+lbox_console_add_history(struct lua_State *L)
+{
+	if (lua_gettop(L) < 1 || !lua_isstring(L, 1))
+		luaL_error(L, "add_history(string)");
+
+	add_history(lua_tostring(L, 1));
+	return 0;
+}
+
+void
+tarantool_lua_console_init(struct lua_State *L)
+{
+	static const struct luaL_reg consolelib[] = {
+		{"add_history",        lbox_console_add_history},
+		{"completion_handler", lbox_console_completion_handler},
+		{NULL, NULL}
+	};
+	luaL_register_module(L, "console", consolelib);
+
+	/* readline() func neads a ref to completion_handler (in upvalue) */
+	lua_getfield(L, -1, "completion_handler");
+	lua_pushcclosure(L, lbox_console_readline, 1);
+	lua_setfield(L, -2, "readline");
+}
+
+/*
+ * Completion engine from "Mike Paul's advanced readline patch".
+ * With minor fixes and code style tweaks.
+ */
+#define lua_pushglobaltable(L) lua_pushvalue(L, LUA_GLOBALSINDEX)
+
+enum {
+	/*
+	 * Suggest a keyword if a prefix of KEYWORD_MATCH_MIN
+	 * characters or more was entered.
+	 */
+	KEYWORD_MATCH_MIN = 1,
+	/*
+	 * Metatables are consulted recursively when learning items;
+	 * avoid infinite metatable loops.
+	 */
+	METATABLE_RECURSION_MAX = 20,
+	/*
+	 * Extracting all items matching a given prefix is O(n);
+	 * stop once that many items were considered.
+	 */
+	ITEMS_CHECKED_MAX = 500
+};
+
+/* goto intentionally omited */
+static const char *
+const lua_rl_keywords[] = {
+	"and", "break", "do", "else", "elseif", "end", "false",
+	"for", "function", "if", "in", "local", "nil", "not", "or",
+	"repeat", "return", "then", "true", "until", "while", NULL
+};
+
+static int
+valid_identifier(const char *s)
+{
+	if (!(isalpha(*s) || *s == '_')) return 0;
+	for (s++; *s; s++)
+		if (!(isalpha(*s) || isdigit(*s) || *s == '_')) return 0;
+	return 1;
+}
+
+/*
+ * Dynamically resizable match list.
+ * Readline consumes argv-style string list; both the list itself and
+ * individual strings should be malloc-ed; readline is responsible for
+ * releasing them once done. Item #0 is the longest common prefix
+ * (inited last). Idx is the last index assigned (i.e. len - 1.)
+ */
+typedef struct {
+	char **list;
+	size_t idx, allocated, matchlen;
+} dmlist;
+
+static void
+lua_rl_dmfree(dmlist *ml)
+{
+	size_t i;
+	/*
+	 * Note: item #0 isn't initialized until the very end of
+	 * lua_rl_complete, the only function calling dmfree().
+	 */
+	for (i = 1; i <= ml->idx; i++) {
+		free(ml->list[i]);
+	}
+	free(ml->list);
+	ml->list = NULL;
+}
+
+/* Add prefix + string + suffix to list and compute common prefix. */
+static int
+lua_rl_dmadd(dmlist *ml, const char *p, size_t pn, const char *s, int suf)
+{
+	char *t = NULL;
+
+	if (ml->idx+1 >= ml->allocated) {
+		char **new_list;
+		new_list = realloc(
+			ml->list, sizeof(char *)*(ml->allocated += 32));
+		if (!new_list)
+			return -1;
+		ml->list = new_list;
+	}
+
+	if (s) {
+		size_t n = strlen(s);
+		if (!(t = (char *)malloc(sizeof(char)*(pn + n + 2))))
+			return 1;
+		memcpy(t, p, pn);
+		memcpy(t + pn, s, n);
+		n += pn;
+		t[n] = suf;
+		if (suf) t[++n] = '\0';
+
+		if (ml->idx == 0) {
+			ml->matchlen = n;
+		} else {
+			size_t i;
+			for (i = 0; i < ml->matchlen && i < n &&
+			     ml->list[1][i] == t[i]; i++) ;
+			/* Set matchlen to common prefix. */
+			ml->matchlen = i;
+		}
+	}
+
+	ml->list[++ml->idx] = t;
+	return 0;
+}
+
+/* Get __index field of metatable of object on top of stack. */
+static int
+lua_rl_getmetaindex(lua_State *L)
+{
+	if (!lua_getmetatable(L, -1)) {
+		lua_pop(L, 1);
+		return 0;
+	}
+	lua_pushstring(L, "__index");
+	lua_rawget(L, -2);
+	lua_replace(L, -2);
+	if (lua_isnil(L, -1) || lua_rawequal(L, -1, -2)) {
+		lua_pop(L, 2);
+		return 0;
+	}
+	lua_replace(L, -2);
+	return 1;
+}	 /* 1: obj -- val, 0: obj -- */
+
+/* Get field from object on top of stack. Avoid calling metamethods. */
+static int
+lua_rl_getfield(lua_State *L, const char *s, size_t n)
+{
+	int loop = METATABLE_RECURSION_MAX;
+	do {
+		if (lua_istable(L, -1)) {
+			lua_pushlstring(L, s, n);
+			lua_rawget(L, -2);
+			if (!lua_isnil(L, -1)) {
+				lua_replace(L, -2);
+				return 1;
+			}
+			lua_pop(L, 1);
+		}
+		if (--loop == 0) {
+			lua_pop(L, 1);
+			return 0;
+		}
+	} while (lua_rl_getmetaindex(L));
+	return 0;
+}	 /* 1: obj -- val, 0: obj -- */
+
+static char **
+lua_rl_complete(lua_State *L, const char *text, int start, int end)
+{
+	dmlist ml;
+	const char *s;
+	size_t i, n, dot, items_checked;
+	int loop, savetop, is_method_ref = 0;
+
+	if (!(text[0] == '\0' || isalpha(text[0]) || text[0] == '_'))
+		return NULL;
+
+	ml.list = NULL;
+	ml.idx = ml.allocated = ml.matchlen = 0;
+
+	savetop = lua_gettop(L);
+	lua_pushglobaltable(L);
+	for (n = (size_t)(end-start), i = dot = 0; i < n; i++) {
+		if (text[i] == '.' || text[i] == ':') {
+			is_method_ref = (text[i] == ':');
+			if (!lua_rl_getfield(L, text+dot, i-dot))
+				goto error; /* Invalid prefix. */
+			dot = i+1;
+			/* Points to first char after dot/colon. */
+		}
+	}
+
+	/* Add all matches against keywords if there is no dot/colon. */
+	if (dot == 0) {
+		for (i = 0; (s = lua_rl_keywords[i]) != NULL; i++) {
+			if (n >= KEYWORD_MATCH_MIN &&
+			    !strncmp(s, text, n) &&
+			    lua_rl_dmadd(&ml, NULL, 0, s, ' ')) {
+
+				goto error;
+			}
+		}
+	}
+
+	/* Add all valid matches from all tables/metatables. */
+	loop = 0;
+	items_checked = 0;
+	lua_pushglobaltable(L);
+	lua_insert(L, -2);
+	do {
+		if (!lua_istable(L, -1) ||
+		    (loop != 0 && lua_rawequal(L, -1, -2)))
+			continue;
+
+		for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
+
+			/* Beware huge tables */
+			if (++items_checked > ITEMS_CHECKED_MAX)
+				break;
+
+			if (lua_type(L, -2) != LUA_TSTRING)
+				continue;
+
+			s = lua_tostring(L, -2);
+			/*
+			 * Only match names starting with '_'
+			 * if explicitly requested.
+			 */
+			if (strncmp(s, text+dot, n-dot) ||
+			    !valid_identifier(s) ||
+			    (*s == '_' && text[dot] != '_')) continue;
+
+			int suf = 0; /* Omit suffix by default. */
+			int type = lua_type(L, -1);
+			switch (type) {
+			case LUA_TTABLE:
+			case LUA_TUSERDATA:
+				/*
+				 * For tables and userdata omit a
+				 * suffix, since all variants, i.e.
+				 * T, T.field, T:method and T()
+				 * are likely valid.
+				 */
+				break;
+			case LUA_TFUNCTION:
+				/*
+				 * Prepend '(' for a function. This
+				 * helps to differentiate functions
+				 * visually in completion lists. It is
+				 * believed that in interactive console
+				 * functions are most often called
+				 * rather then assigned to a variable or
+				 * passed as a parameter, hence
+				 * an ocasional need to delete an
+				 * unwanted '(' shouldn't be a burden.
+				 */
+				suf = '(';
+				break;
+			}
+			/*
+			 * If completing a method ref, i.e
+			 * foo:meth<TAB>, show functions only.
+			 */
+			if (!is_method_ref || type == LUA_TFUNCTION) {
+				if (lua_rl_dmadd(&ml, text, dot, s, suf))
+					goto error;
+			}
+		}
+	} while (++loop < METATABLE_RECURSION_MAX && lua_rl_getmetaindex(L));
+
+	lua_pop(L, 1);
+
+	if (ml.idx == 0) {
+error:
+		lua_rl_dmfree(&ml);
+		lua_settop(L, savetop);
+		return NULL;
+	} else {
+		/* list[0] holds the common prefix of all matches (may
+		 * be ""). If there is only one match, list[0] and
+		 * list[1] will be the same. */
+		ml.list[0] = malloc(sizeof(char)*(ml.matchlen+1));
+		if (!ml.list[0])
+			goto error;
+		memcpy(ml.list[0], ml.list[1], ml.matchlen);
+		ml.list[0][ml.matchlen] = '\0';
+		/* Add the NULL list terminator. */
+		if (lua_rl_dmadd(&ml, NULL, 0, NULL, 0)) goto error;
+	}
+
+	lua_settop(L, savetop);
+#if RL_READLINE_VERSION >= 0x0600
+	rl_completion_suppress_append = 1;
+#endif
+	return ml.list;
+}
diff --git a/src/lua/console.h b/src/lua/console.h
new file mode 100644
index 0000000000000000000000000000000000000000..208b314909eadf7ba682e97f26a1f48b0dda5e8a
--- /dev/null
+++ b/src/lua/console.h
@@ -0,0 +1,46 @@
+#ifndef TARANTOOL_LUA_CONSOLE_H_INCLUDED
+#define TARANTOOL_LUA_CONSOLE_H_INCLUDED
+/*
+ * Copyright 2010-2016, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct lua_State;
+
+void
+tarantool_lua_console_init(struct lua_State *L);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
+
+#endif /* TARANTOOL_LUA_CONSOLE_H_INCLUDED */
diff --git a/src/lua/console.lua b/src/lua/console.lua
index 1a2fb26dfeea8a0804ff5c1269c59478d28694e8..f4be9170a9b8ce9705267c29b1a8941288c11733 100644
--- a/src/lua/console.lua
+++ b/src/lua/console.lua
@@ -94,6 +94,7 @@ local function remote_eval(self, line)
         self.remote = nil
         self.eval = nil
         self.prompt = nil
+        self.completion = nil
         return ""
     end
     --
@@ -123,7 +124,10 @@ local function local_read(self)
     local prompt = self.prompt
     while true do
         local delim = self.delimiter
-        local line = internal.readline(prompt.. "> ")
+        local line = internal.readline({
+            prompt = prompt.. "> ",
+            completion = self.completion
+        })
         if not line then
             return nil
         end
@@ -207,6 +211,7 @@ local repl_mt = {
         read = local_read;
         eval = local_eval;
         print = local_print;
+        completion = internal.completion_handler;
     };
 }
 
@@ -319,6 +324,7 @@ local function connect(uri)
     self.remote = remote
     self.eval = remote_eval
     self.prompt = string.format("%s:%s", self.remote.host, self.remote.port)
+    self.completion = function () end -- no completion for remote console
     log.info("connected to %s:%s", self.remote.host, self.remote.port)
     return true
 end
diff --git a/src/lua/init.c b/src/lua/init.c
index 92c75c45d6d729ac673d97a9ba143d09fbf6bf21..3f7b9c6a7b238ca76ab7f5999960ad62ad33d8c4 100644
--- a/src/lua/init.c
+++ b/src/lua/init.c
@@ -42,7 +42,8 @@
 #include <luajit.h>
 
 #include <fiber.h>
-#include "coeio.h"
+#include "coio.h"
+#include "lua/console.h"
 #include "lua/fiber.h"
 #include "lua/ipc.h"
 #include "lua/errno.h"
@@ -193,74 +194,6 @@ lbox_coredump(struct lua_State *L __attribute__((unused)))
 
 /* }}} */
 
-/*
- * {{{ console library
- */
-
-static ssize_t
-readline_cb(va_list ap)
-{
-	const char **line = va_arg(ap, const char **);
-	const char *prompt = va_arg(ap, const char *);
-	/*
-	 * libeio threads blocks all signals by default. Therefore, nobody
-	 * can interrupt read(2) syscall inside readline() to correctly
-	 * cleanup resources and restore terminal state. In case of signal
-	 * a signal_cb(), a ev watcher in tarantool.cc will stop event
-	 * loop and and stop entire process by exiting the main thread.
-	 * rl_cleanup_after_signal() is called from tarantool_lua_free()
-	 * in order to restore terminal state.
-	 */
-	*line = readline(prompt);
-	return 0;
-}
-
-static int
-tarantool_console_readline(struct lua_State *L)
-{
-	const char *prompt = ">";
-	if (lua_gettop(L) > 0) {
-		if (!lua_isstring(L, 1))
-			luaL_error(L, "console.readline([prompt])");
-		prompt = lua_tostring(L, 1);
-	}
-
-	char *line;
-	if (coio_call(readline_cb, &line, prompt) != 0) {
-		lua_pushnil(L);
-		return 1;
-	}
-
-	if (!line) {
-		lua_pushnil(L);
-	} else {
-		/* Make sure the line doesn't leak. */
-		static __thread char *line_buf = NULL;
-		if (line_buf)
-			free(line_buf);
-
-		line_buf = line;
-		lua_pushstring(L, line);
-
-		free(line_buf);
-		line_buf = NULL;
-	}
-
-	return 1;
-}
-
-static int
-tarantool_console_add_history(struct lua_State *L)
-{
-	if (lua_gettop(L) < 1 || !lua_isstring(L, 1))
-		luaL_error(L, "console.add_history(string)");
-
-	add_history(lua_tostring(L, 1));
-	return 0;
-}
-
-/* }}} */
-
 /**
  * Prepend the variable list of arguments to the Lua
  * package search path
@@ -395,12 +328,7 @@ tarantool_lua_init(const char *tarantool_bin, int argc, char **argv)
 	rl_catch_signals = 0;
 	rl_catch_sigwinch = 0;
 #endif
-	static const struct luaL_reg consolelib[] = {
-		{"readline", tarantool_console_readline},
-		{"add_history", tarantool_console_add_history},
-		{NULL, NULL}
-	};
-	luaL_register_module(L, "console", consolelib);
+	tarantool_lua_console_init(L);
 	lua_pop(L, 1);
 
 	lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED");
@@ -557,15 +485,4 @@ tarantool_lua_free()
 		lua_close(tarantool_L);
 	}
 	tarantool_L = NULL;
-
-#if 0
-	/* Temporarily moved to tarantool_free(), tarantool_lua_free() not
-	 * being called due to cleanup order issues
-	 */
-	if (isatty(STDIN_FILENO)) {
-		/* See comments in readline_cb() */
-		rl_cleanup_after_signal();
-	}
-#endif
 }
-
diff --git a/src/main.cc b/src/main.cc
index d12d36f874cefec5058eef986199f210caecf852..edb82198825aedc34e8734622b1e1e1cf0bea9a7 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -495,13 +495,6 @@ tarantool_free(void)
 	}
 	if (script)
 		free(script);
-	/* tarantool_lua_free() was formerly reponsible for terminal reset,
-	 * but it is no longer called
-	 */
-	if (isatty(STDIN_FILENO)) {
-		/* See comments in readline_cb() */
-		rl_cleanup_after_signal();
-	}
 #ifdef HAVE_BFD
 	symbols_free();
 #endif