diff --git a/Documentation/Filters/Luafilter.md b/Documentation/Filters/Luafilter.md
index c9e9d31065..af5de34ee0 100644
--- a/Documentation/Filters/Luafilter.md
+++ b/Documentation/Filters/Luafilter.md
@@ -54,7 +54,7 @@ The entry points for the Lua script expect the following signatures:
 
     - The `closeSession` function in the Lua scripts will be called.
 
-  - `(nil | bool | string) routeQuery(string)` - query is being routed
+  - `(nil | bool | string) routeQuery()` - query is being routed
 
     - The Luafilter calls the `routeQuery` functions of both the session and the
       global script.  The query is passed as a string parameter to the
@@ -65,7 +65,7 @@ The entry points for the Lua script expect the following signatures:
       is replaced with the return value and the query will be routed. If nil is
       returned, the query is routed normally.
 
-  - `nil clientReply(string)` - reply to a query is being routed
+  - `nil clientReply()` - reply to a query is being routed
 
     - This function is called with the name of the server that returned the response.
 
@@ -97,11 +97,11 @@ function closeSession()
 
 end
 
-function routeQuery(query)
+function routeQuery()
 
 end
 
-function clientReply(server)
+function clientReply()
 
 end
 
@@ -113,9 +113,17 @@ end
 ### Functions Exposed by the Luafilter
 
 The luafilter exposes the following functions that can be called inside the Lua
-script API endpoints.
+script API endpoints. The callback function in which they can be called is
+documented after the function signature. If the functions are called outside of
+the correct callback function, they raise a Lua error.
 
-- `string mxs_get_type_mask()`
+- `string mxs_get_sql()` (use: `routeQuery`)
+
+  - Returns the SQL of the query being executed. This returns an empty string
+    for any query that is not a text protocol query (COM_QUERY). Support for
+    prepared statements is not yet implemented.
+
+- `string mxs_get_type_mask()` (use: `routeQuery`)
 
   - Returns the type of the current query being executed as a string. The values
     are the string versions of the query types defined in _query_classifier.h_
@@ -123,37 +131,41 @@ script API endpoints.
 
     This function can only be called from the `routeQuery` entry point.
 
-- `string mxs_get_operation()`
+- `string mxs_get_operation()` (use: `routeQuery`)
 
   - Returns the current operation type as a string. The values are defined in
     _query_classifier.h_.
 
     This function can only be called from the `routeQuery` entry point.
 
-- `string mxs_get_canonical()`
+- `string mxs_get_canonical()` (use: `routeQuery`)
 
   - Returns the canonical version of a query by replacing all user-defined constant values with question marks.
 
     This function can only be called from the `routeQuery` entry point.
 
-- `number mxs_get_session_id()`
+- `number mxs_get_session_id()` (use: `newSession`, `routeQuery`, `clientReply`, `closeSession`)
 
   - This function returns the session ID of the current session. Inside the
     `createInstance` and `diagnostic` endpoints this function will always return
     the value 0.
 
-- `string mxs_get_db()`
+- `string mxs_get_db()` (use: `newSession`, `routeQuery`, `clientReply`, `closeSession`)
 
   - Returns the current default database used by the connection.
 
-- `string mxs_get_user()`
+- `string mxs_get_user()` (use: `newSession`, `routeQuery`, `clientReply`, `closeSession`)
 
   - Returns the username of the client connection.
 
-- `string mxs_get_host()`
+- `string mxs_get_host()` (use: `newSession`, `routeQuery`, `clientReply`, `closeSession`)
 
   - Returns the address of the client connection.
 
+- `string mxs_get_replier()` (use: `clientReply`)
+
+  - Returns the target that returned the result to the latest query.
+
 ## Example Configuration and Script
 
 Here is a minimal configuration entry for a luafilter definition.
@@ -182,12 +194,12 @@ function closeSession()
     f:write("closeSession\n")
 end
 
-function routeQuery(query)
-    f:write("routeQuery: " .. query .. " -- type: " .. mxs_qc_get_type_mask() .. " operation: " .. mxs_qc_get_operation() .. "\n")
+function routeQuery()
+    f:write("routeQuery: " .. mxs_get_sql() .. " -- type: " .. mxs_qc_get_type_mask() .. " operation: " .. mxs_qc_get_operation() .. "\n")
 end
 
-function clientReply(server)
-    f:write("clientReply: " .. server .. "\n")
+function clientReply()
+    f:write("clientReply: " .. mxs_get_replier() .. "\n")
 end
 
 function diagnostic()
@@ -196,3 +208,12 @@ function diagnostic()
 end
 
 ```
+
+## Limitations
+
+* `mxs_get_sql()` and `mxs_get_canonical()` do not work with queries done with
+  the binary protocol.
+
+* The Lua code is not restricted in any way which means excessively slow
+  execution of it can cause the MaxScale process to become slower or to be
+  aborted due to a SystemD watchdog timeout.
\ No newline at end of file
diff --git a/server/modules/filter/luafilter/luacontext.cc b/server/modules/filter/luafilter/luacontext.cc
index 9f8d5200cb..fc9039818b 100644
--- a/server/modules/filter/luafilter/luacontext.cc
+++ b/server/modules/filter/luafilter/luacontext.cc
@@ -22,13 +22,31 @@
 namespace
 {
 
+const char* CN_CREATE_INSTANCE = "createInstance";
+const char* CN_NEW_SESSON = "newSession";
+const char* CN_ROUTE_QUERY = "routeQuery";
+const char* CN_CLIENT_REPLY = "clientReply";
+const char* CN_CLOSE_SESSION = "closeSession";
+const char* CN_DIAGNOSTIC = "diagnostic";
+
 //
 // Helpers for dealing with the Lua C API
 //
 
+inline void check_precondition(lua_State* state, bool value)
+{
+    if (!value)
+    {
+        lua_pushstring(state, "Function called outside of the correct callback function");
+        lua_error(state);   // lua_error does a longjump and doesn't return
+    }
+}
+
 const LuaData& get_data(lua_State* state)
 {
-    return *static_cast<LuaData*>(lua_touserdata(state, lua_upvalueindex(1)));
+    LuaData* data = static_cast<LuaData*>(lua_touserdata(state, lua_upvalueindex(1)));
+    check_precondition(state, data);
+    return *data;
 }
 
 void push_arg(lua_State* state, std::string_view str)
@@ -41,6 +59,25 @@ void push_arg(lua_State* state, int64_t i)
     lua_pushinteger(state, i);
 }
 
+template<class ... Args>
+bool call_pushed_function(lua_State* state, const char* name, int nret, Args ... args)
+{
+    bool ok = true;
+    mxb_assert(lua_type(state, -1) == LUA_TFUNCTION);
+
+    (push_arg(state, std::forward<Args>(args)), ...);
+    constexpr int nargs = sizeof...(Args);
+
+    if (lua_pcall(state, nargs, nret, 0))
+    {
+        MXB_WARNING("The call to '%s' failed: %s", name, lua_tostring(state, -1));
+        lua_pop(state, -1);     // Pop the error off the stack
+        ok = false;
+    }
+
+    return ok;
+}
+
 template<class ... Args>
 bool call_function(lua_State* state, const char* name, int nret, Args ... args)
 {
@@ -56,125 +93,140 @@ bool call_function(lua_State* state, const char* name, int nret, Args ... args)
     }
     else
     {
-        (push_arg(state, std::forward<Args>(args)), ...);
-        constexpr int nargs = sizeof...(Args);
-
-        if (lua_pcall(state, nargs, nret, 0))
-        {
-            MXB_WARNING("The call to '%s' failed: %s", name, lua_tostring(state, -1));
-            lua_pop(state, -1);     // Pop the error off the stack
-            ok = false;
-        }
+        ok = call_pushed_function(state, name, nret, std::forward<Args>(args)...);
     }
 
     return ok;
 }
 
+template<class ... Args>
+bool call_function(lua_State* state, int ref, const char* name, int nret, Args ... args)
+{
+    if (ref == LUA_REFNIL)
+    {
+        return false;
+    }
+
+    MXB_AT_DEBUG(int type = ) lua_rawgeti(state, LUA_REGISTRYINDEX, ref);
+    mxb_assert(type == LUA_TFUNCTION);
+    return call_pushed_function(state, name, nret, std::forward<Args>(args)...);
+}
+
+int get_function_ref(lua_State* state, const char* name)
+{
+    int rv = LUA_REFNIL;
+    lua_getglobal(state, name);
+    int type = lua_type(state, -1);
+
+    if (type != LUA_TFUNCTION)
+    {
+        MXB_WARNING("The '%s' global is not a function but a %s", name, lua_typename(state, type));
+        lua_pop(state, -1);     // Pop the value off the stack
+    }
+    else
+    {
+        rv = luaL_ref(state, LUA_REGISTRYINDEX);
+    }
+
+    return rv;
+}
+
 //
 // Functions that are exposed to the Lua environment
 //
 
 static int lua_get_session_id(lua_State* state)
 {
-    uint64_t id = 0;
     const auto& data = get_data(state);
+    check_precondition(state, data.session);
 
-    if (data.session)
-    {
-        id = data.session->id();
-    }
-
-    lua_pushinteger(state, id);
+    lua_pushinteger(state, data.session->id());
     return 1;
 }
 
 static int lua_get_type_mask(lua_State* state)
 {
     const auto& data = get_data(state);
+    check_precondition(state, data.session && data.buffer);
 
-    if (data.buffer)
-    {
-        uint32_t type = data.session->client_connection()->parser()->get_type_mask(*data.buffer);
-        std::string mask = mxs::Parser::type_mask_to_string(type);
-        lua_pushstring(state, mask.c_str());
-    }
-    else
-    {
-        lua_pushliteral(state, "");
-    }
-
+    uint32_t type = data.session->client_connection()->parser()->get_type_mask(*data.buffer);
+    std::string mask = mxs::Parser::type_mask_to_string(type);
+    lua_pushlstring(state, mask.data(), mask.size());
     return 1;
 }
 
 static int lua_get_operation(lua_State* state)
 {
     const auto& data = get_data(state);
-    const char* opstring = "";
-
-    if (data.buffer)
-    {
-        mxs::sql::OpCode op = data.session->client_connection()->parser()->get_operation(*data.buffer);
-        opstring = mxs::sql::to_string(op);
-    }
+    check_precondition(state, data.session && data.buffer);
 
-    lua_pushstring(state, opstring);
+    mxs::sql::OpCode op = data.session->client_connection()->parser()->get_operation(*data.buffer);
+    lua_pushstring(state, mxs::sql::to_string(op));
     return 1;
 }
 
 static int lua_get_canonical(lua_State* state)
 {
     const auto& data = get_data(state);
-    std::string sql;
+    check_precondition(state, data.session && data.buffer);
+
+    auto sql = data.session->client_connection()->parser()->get_canonical(*data.buffer);
+    lua_pushlstring(state, sql.data(), sql.size());
 
-    if (data.session && data.buffer)
-    {
-        sql = std::string(data.session->client_connection()->parser()->get_sql(*data.buffer));
-        maxsimd::get_canonical(&sql);
-    }
 
-    lua_pushstring(state, sql.c_str());
     return 1;
 }
 
 static int lua_get_db(lua_State* state)
 {
     const auto& data = get_data(state);
-    std::string db;
+    check_precondition(state, data.session);
 
-    if (data.session)
-    {
-        db = data.session->client_connection()->current_db();
-    }
+    std::string db = data.session->client_connection()->current_db();
+    lua_pushlstring(state, db.data(), db.size());
 
-    lua_pushstring(state, db.c_str());
     return 1;
 }
 
 static int lua_get_user(lua_State* state)
 {
     const auto& data = get_data(state);
-    std::string user;
+    check_precondition(state, data.session);
 
-    if (data.session)
-    {
-        user = data.session->user();
-    }
+    const auto& user = data.session->user();
+    lua_pushlstring(state, user.data(), user.size());
 
-    lua_pushstring(state, user.c_str());
     return 1;
 }
 
 static int lua_get_host(lua_State* state)
 {
     const auto& data = get_data(state);
-    std::string remote;
+    check_precondition(state, data.session);
 
-    if (data.session)
-    {
-        remote = data.session->client_remote();
-    }
+    const auto& remote = data.session->client_remote();
+    lua_pushlstring(state, remote.data(), remote.size());
 
-    lua_pushstring(state, remote.c_str());
+    return 1;
+}
+
+static int lua_get_sql(lua_State* state)
+{
+    const auto& data = get_data(state);
+    check_precondition(state, data.session && data.buffer);
+
+    auto sql = data.session->client_connection()->parser()->get_sql(*data.buffer);
+    lua_pushlstring(state, sql.data(), sql.size());
+
+    return 1;
+}
+
+static int lua_get_replier(lua_State* state)
+{
+    const auto& data = get_data(state);
+    check_precondition(state, data.target);
+
+    lua_pushstring(state, data.target);
     return 1;
 }
 }
@@ -236,31 +288,50 @@ LuaContext::LuaContext(lua_State* state)
     lua_pushlightuserdata(m_state, &m_data);
     lua_pushcclosure(m_state, lua_get_host, 1);
     lua_setglobal(m_state, "mxs_get_host");
+
+    lua_pushlightuserdata(m_state, &m_data);
+    lua_pushcclosure(m_state, lua_get_sql, 1);
+    lua_setglobal(m_state, "mxs_get_sql");
+
+    lua_pushlightuserdata(m_state, &m_data);
+    lua_pushcclosure(m_state, lua_get_replier, 1);
+    lua_setglobal(m_state, "mxs_get_replier");
+
+    m_client_reply = get_function_ref(m_state, "clientReply");
+    m_route_query = get_function_ref(m_state, "routeQuery");
 }
 
 LuaContext::~LuaContext()
 {
+    for (int ref : {m_client_reply, m_route_query})
+    {
+        if (ref != LUA_REFNIL)
+        {
+            luaL_unref(m_state, LUA_REGISTRYINDEX, ref);
+        }
+    }
+
     lua_close(m_state);
 }
 
 void LuaContext::create_instance(const std::string& name)
 {
-    call_function(m_state, "createInstance", 0, name);
+    m_data = LuaData{nullptr, nullptr, nullptr};
+    call_function(m_state, CN_CREATE_INSTANCE, 0, name);
 }
 
 void LuaContext::new_session(MXS_SESSION* session)
 {
-    Scope scope(this, {session, nullptr});
-    call_function(m_state, "newSession", 0, session->user(), session->client_remote());
+    m_data = LuaData{session, nullptr, nullptr};
+    call_function(m_state, CN_NEW_SESSON, 0, session->user(), session->client_remote());
 }
 
 bool LuaContext::route_query(MXS_SESSION* session, GWBUF* buffer)
 {
-    Scope scope(this, {session, buffer});
+    m_data = LuaData{session, buffer, nullptr};
     bool route = true;
-    std::string_view sql = session->protocol()->get_sql(*m_data.buffer);
 
-    if (call_function(m_state, "routeQuery", 1, sql))
+    if (call_function(m_state, m_route_query, CN_ROUTE_QUERY, 1))
     {
         if (lua_gettop(m_state))
         {
@@ -280,21 +351,22 @@ bool LuaContext::route_query(MXS_SESSION* session, GWBUF* buffer)
 
 void LuaContext::client_reply(MXS_SESSION* session, const char* target)
 {
-    Scope scope(this, {session, nullptr});
-    call_function(m_state, "clientReply", 0, target);
+    m_data = LuaData{session, nullptr, target};
+    call_function(m_state, m_client_reply, CN_CLIENT_REPLY, 0);
 }
 
 void LuaContext::close_session(MXS_SESSION* session)
 {
-    Scope scope(this, {session, nullptr});
-    call_function(m_state, "closeSession", 0);
+    m_data = LuaData{session, nullptr, nullptr};
+    call_function(m_state, CN_CLOSE_SESSION, 0);
 }
 
 std::string LuaContext::diagnostics()
 {
     std::string rval;
+    m_data = LuaData{nullptr, nullptr, nullptr};
 
-    if (call_function(m_state, "diagnostic", 1))
+    if (call_function(m_state, CN_DIAGNOSTIC, 1))
     {
         lua_gettop(m_state);
 
diff --git a/server/modules/filter/luafilter/luacontext.hh b/server/modules/filter/luafilter/luacontext.hh
index a987c87306..9f00e4db98 100644
--- a/server/modules/filter/luafilter/luacontext.hh
+++ b/server/modules/filter/luafilter/luacontext.hh
@@ -30,6 +30,7 @@ struct LuaData
 {
     MXS_SESSION* session = nullptr;
     GWBUF*       buffer = nullptr;
+    const char*  target = nullptr;
 };
 
 class LuaContext
@@ -50,24 +51,10 @@ public:
 private:
     LuaContext(lua_State* state);
 
-    // Helper class for making sure the data is reset to a known good state after each function call
-    struct Scope
-    {
-        Scope(LuaContext* ctx, LuaData new_data)
-            : m_ctx(ctx)
-            , m_data(std::exchange(ctx->m_data, new_data))
-        {
-        }
-
-        ~Scope()
-        {
-            m_ctx->m_data = m_data;
-        }
-
-        LuaContext* m_ctx;
-        LuaData     m_data;
-    };
-
     LuaData    m_data;
     lua_State* m_state {nullptr};
+
+    // References to the commonly used global functions
+    int m_route_query {LUA_REFNIL};
+    int m_client_reply {LUA_REFNIL};
 };
