diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 0c7bbbecf..08554ce28 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -134,6 +134,8 @@ add_library(core STATIC
     hle/kernel/hle_ipc.h
     hle/kernel/ipc.cpp
     hle/kernel/ipc.h
+    hle/kernel/ipc_debugger/recorder.cpp
+    hle/kernel/ipc_debugger/recorder.h
     hle/kernel/kernel.cpp
     hle/kernel/kernel.h
     hle/kernel/memory.cpp
diff --git a/src/core/hle/kernel/ipc_debugger/recorder.cpp b/src/core/hle/kernel/ipc_debugger/recorder.cpp
new file mode 100644
index 000000000..968815c5b
--- /dev/null
+++ b/src/core/hle/kernel/ipc_debugger/recorder.cpp
@@ -0,0 +1,165 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+#include "core/hle/kernel/client_port.h"
+#include "core/hle/kernel/client_session.h"
+#include "core/hle/kernel/ipc_debugger/recorder.h"
+#include "core/hle/kernel/process.h"
+#include "core/hle/kernel/server_port.h"
+#include "core/hle/kernel/server_session.h"
+#include "core/hle/kernel/session.h"
+#include "core/hle/kernel/thread.h"
+#include "core/hle/service/service.h"
+
+namespace IPCDebugger {
+
+namespace {
+ObjectInfo GetObjectInfo(const Kernel::Object* object) {
+    if (object == nullptr) {
+        return {};
+    }
+    return {object->GetTypeName(), object->GetName(), static_cast<int>(object->GetObjectId())};
+}
+
+ObjectInfo GetObjectInfo(const Kernel::Thread* thread) {
+    if (thread == nullptr) {
+        return {};
+    }
+    return {thread->GetTypeName(), thread->GetName(), static_cast<int>(thread->GetThreadId())};
+}
+
+ObjectInfo GetObjectInfo(const Kernel::Process* process) {
+    if (process == nullptr) {
+        return {};
+    }
+    return {process->GetTypeName(), process->GetName(), static_cast<int>(process->process_id)};
+}
+} // namespace
+
+Recorder::Recorder() = default;
+Recorder::~Recorder() = default;
+
+bool Recorder::IsEnabled() const {
+    return enabled.load(std::memory_order_relaxed);
+}
+
+void Recorder::RegisterRequest(const std::shared_ptr<Kernel::ClientSession>& client_session,
+                               const std::shared_ptr<Kernel::Thread>& client_thread) {
+    const u32 thread_id = client_thread->GetThreadId();
+
+    RequestRecord record = {/* id */ ++record_count,
+                            /* status */ RequestStatus::Sent,
+                            /* client_process */ GetObjectInfo(client_thread->owner_process),
+                            /* client_thread */ GetObjectInfo(client_thread.get()),
+                            /* client_session */ GetObjectInfo(client_session.get()),
+                            /* client_port */ GetObjectInfo(client_session->parent->port.get()),
+                            /* server_process */ {},
+                            /* server_thread */ {},
+                            /* server_session */ GetObjectInfo(client_session->parent->server)};
+    record_map.insert_or_assign(thread_id, std::make_unique<RequestRecord>(record));
+    client_session_map.insert_or_assign(thread_id, client_session);
+
+    InvokeCallbacks(record);
+}
+
+void Recorder::SetRequestInfo(const std::shared_ptr<Kernel::Thread>& client_thread,
+                              std::vector<u32> untranslated_cmdbuf,
+                              std::vector<u32> translated_cmdbuf,
+                              const std::shared_ptr<Kernel::Thread>& server_thread) {
+    const u32 thread_id = client_thread->GetThreadId();
+    if (!record_map.count(thread_id)) {
+        // This is possible when the recorder is enabled after application started
+        LOG_ERROR(Kernel, "No request is assoicated with the thread");
+        return;
+    }
+
+    auto& record = *record_map[thread_id];
+    record.status = RequestStatus::Handling;
+    record.untranslated_request_cmdbuf = std::move(untranslated_cmdbuf);
+    record.translated_request_cmdbuf = std::move(translated_cmdbuf);
+
+    if (server_thread) {
+        record.server_process = GetObjectInfo(server_thread->owner_process);
+        record.server_thread = GetObjectInfo(server_thread.get());
+    } else {
+        record.is_hle = true;
+    }
+
+    // Function name
+    ASSERT_MSG(client_session_map.count(thread_id), "Client session is missing");
+    const auto& client_session = client_session_map[thread_id];
+    if (client_session->parent->port &&
+        client_session->parent->port->GetServerPort()->hle_handler) {
+
+        record.function_name = std::dynamic_pointer_cast<Service::ServiceFrameworkBase>(
+                                   client_session->parent->port->GetServerPort()->hle_handler)
+                                   ->GetFunctionName(record.untranslated_request_cmdbuf[0]);
+    }
+    client_session_map.erase(thread_id);
+
+    InvokeCallbacks(record);
+}
+
+void Recorder::SetReplyInfo(const std::shared_ptr<Kernel::Thread>& client_thread,
+                            std::vector<u32> untranslated_cmdbuf,
+                            std::vector<u32> translated_cmdbuf) {
+    const u32 thread_id = client_thread->GetThreadId();
+    if (!record_map.count(thread_id)) {
+        // This is possible when the recorder is enabled after application started
+        LOG_ERROR(Kernel, "No request is assoicated with the thread");
+        return;
+    }
+
+    auto& record = *record_map[thread_id];
+    if (record.status != RequestStatus::HLEUnimplemented) {
+        record.status = RequestStatus::Handled;
+    }
+
+    record.untranslated_reply_cmdbuf = std::move(untranslated_cmdbuf);
+    record.translated_reply_cmdbuf = std::move(translated_cmdbuf);
+    InvokeCallbacks(record);
+
+    record_map.erase(thread_id);
+}
+
+void Recorder::SetHLEUnimplemented(const std::shared_ptr<Kernel::Thread>& client_thread) {
+    const u32 thread_id = client_thread->GetThreadId();
+    if (!record_map.count(thread_id)) {
+        // This is possible when the recorder is enabled after application started
+        LOG_ERROR(Kernel, "No request is assoicated with the thread");
+        return;
+    }
+
+    auto& record = *record_map[thread_id];
+    record.status = RequestStatus::HLEUnimplemented;
+}
+
+CallbackHandle Recorder::BindCallback(CallbackType callback) {
+    std::unique_lock lock(callback_mutex);
+    CallbackHandle handle = std::make_shared<CallbackType>(callback);
+    callbacks.emplace(handle);
+    return handle;
+}
+
+void Recorder::UnbindCallback(const CallbackHandle& handle) {
+    std::unique_lock lock(callback_mutex);
+    callbacks.erase(handle);
+}
+
+void Recorder::InvokeCallbacks(const RequestRecord& request) {
+    {
+        std::shared_lock lock(callback_mutex);
+        for (const auto& iter : callbacks) {
+            (*iter)(request);
+        }
+    }
+}
+
+void Recorder::SetEnabled(bool enabled_) {
+    enabled.store(enabled_, std::memory_order_relaxed);
+}
+
+} // namespace IPCDebugger
diff --git a/src/core/hle/kernel/ipc_debugger/recorder.h b/src/core/hle/kernel/ipc_debugger/recorder.h
new file mode 100644
index 000000000..ebd8bf5d1
--- /dev/null
+++ b/src/core/hle/kernel/ipc_debugger/recorder.h
@@ -0,0 +1,129 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <atomic>
+#include <functional>
+#include <memory>
+#include <set>
+#include <shared_mutex>
+#include <string>
+#include <unordered_map>
+#include <vector>
+#include "common/common_types.h"
+
+namespace Kernel {
+class ClientSession;
+class Thread;
+} // namespace Kernel
+
+namespace IPCDebugger {
+
+/**
+ * Record of a kernel object, for debugging purposes.
+ */
+struct ObjectInfo {
+    std::string type;
+    std::string name;
+    int id = -1;
+};
+
+/**
+ * Status of a request.
+ */
+enum class RequestStatus {
+    Invalid,          ///< Invalid status
+    Sent,             ///< The request is sent to the kernel and is waiting to be handled
+    Handling,         ///< The request is being handled
+    Handled,          ///< The request is handled with reply sent
+    HLEUnimplemented, ///< The request is unimplemented by HLE, and unhandled
+};
+
+/**
+ * Record of an IPC request.
+ */
+struct RequestRecord {
+    int id;
+    RequestStatus status = RequestStatus::Invalid;
+    ObjectInfo client_process;
+    ObjectInfo client_thread;
+    ObjectInfo client_session;
+    ObjectInfo client_port;    // Not available for portless
+    ObjectInfo server_process; // Only available for LLE requests
+    ObjectInfo server_thread;  // Only available for LLE requests
+    ObjectInfo server_session;
+    std::string function_name; // Not available for LLE or portless
+    bool is_hle = false;
+    // Request info is only available when status is not `Invalid` or `Sent`
+    std::vector<u32> untranslated_request_cmdbuf;
+    std::vector<u32> translated_request_cmdbuf;
+    // Reply info is only available when status is `Handled`
+    std::vector<u32> untranslated_reply_cmdbuf;
+    std::vector<u32> translated_reply_cmdbuf;
+};
+
+using CallbackType = std::function<void(const RequestRecord&)>;
+using CallbackHandle = std::shared_ptr<CallbackType>;
+
+class Recorder {
+public:
+    explicit Recorder();
+    ~Recorder();
+
+    /**
+     * Returns whether the recorder is enabled.
+     */
+    bool IsEnabled() const;
+
+    /**
+     * Registers a request into the recorder. The request is then assoicated with the client thread.
+     */
+    void RegisterRequest(const std::shared_ptr<Kernel::ClientSession>& client_session,
+                         const std::shared_ptr<Kernel::Thread>& client_thread);
+
+    /**
+     * Sets the request information of the request record associated with the client thread.
+     * When the server thread is empty, the request will be considered HLE.
+     */
+    void SetRequestInfo(const std::shared_ptr<Kernel::Thread>& client_thread,
+                        std::vector<u32> untranslated_cmdbuf, std::vector<u32> translated_cmdbuf,
+                        const std::shared_ptr<Kernel::Thread>& server_thread = {});
+
+    /**
+     * Sets the reply information of the request record assoicated with the client thread.
+     * The request is then unlinked from the client thread.
+     */
+    void SetReplyInfo(const std::shared_ptr<Kernel::Thread>& client_thread,
+                      std::vector<u32> untranslated_cmdbuf, std::vector<u32> translated_cmdbuf);
+
+    /**
+     * Set the status of a record to HLEUnimplemented.
+     */
+    void SetHLEUnimplemented(const std::shared_ptr<Kernel::Thread>& client_thread);
+
+    /**
+     * Set the status of the debugger (enabled/disabled).
+     */
+    void SetEnabled(bool enabled);
+
+    CallbackHandle BindCallback(CallbackType callback);
+    void UnbindCallback(const CallbackHandle& handle);
+
+private:
+    void InvokeCallbacks(const RequestRecord& request);
+
+    std::unordered_map<u32, std::unique_ptr<RequestRecord>> record_map;
+    int record_count{};
+
+    // Temporary client session map for function name handling
+    std::unordered_map<u32, std::shared_ptr<Kernel::ClientSession>> client_session_map;
+
+    std::atomic_bool enabled{false};
+
+    std::set<CallbackHandle> callbacks;
+    mutable std::shared_mutex callback_mutex;
+};
+
+} // namespace IPCDebugger