diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index 9d5137a4f..0198ada76 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -93,6 +93,9 @@ add_executable(citra-qt
     debugger/ipc/record_dialog.cpp
     debugger/ipc/record_dialog.h
     debugger/ipc/record_dialog.ui
+    debugger/ipc/recorder.cpp
+    debugger/ipc/recorder.h
+    debugger/ipc/recorder.ui
     debugger/lle_service_modules.cpp
     debugger/lle_service_modules.h
     debugger/profiler.cpp
diff --git a/src/citra_qt/debugger/ipc/recorder.cpp b/src/citra_qt/debugger/ipc/recorder.cpp
new file mode 100644
index 000000000..24128e38a
--- /dev/null
+++ b/src/citra_qt/debugger/ipc/recorder.cpp
@@ -0,0 +1,183 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QString>
+#include <QTreeWidgetItem>
+#include <fmt/format.h>
+#include "citra_qt/debugger/ipc/record_dialog.h"
+#include "citra_qt/debugger/ipc/recorder.h"
+#include "common/assert.h"
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/hle/kernel/ipc_debugger/recorder.h"
+#include "core/hle/kernel/kernel.h"
+#include "core/hle/service/sm/sm.h"
+#include "ui_recorder.h"
+
+IPCRecorderWidget::IPCRecorderWidget(QWidget* parent)
+    : QDockWidget(parent), ui(std::make_unique<Ui::IPCRecorder>()) {
+
+    ui->setupUi(this);
+    qRegisterMetaType<IPCDebugger::RequestRecord>();
+
+    connect(ui->enabled, &QCheckBox::stateChanged,
+            [this](int new_state) { SetEnabled(new_state == Qt::Checked); });
+    connect(ui->clearButton, &QPushButton::clicked, this, &IPCRecorderWidget::Clear);
+    connect(ui->filter, &QLineEdit::textChanged, this, &IPCRecorderWidget::ApplyFilterToAll);
+    connect(ui->main, &QTreeWidget::itemDoubleClicked, this, &IPCRecorderWidget::OpenRecordDialog);
+    connect(this, &IPCRecorderWidget::EntryUpdated, this, &IPCRecorderWidget::OnEntryUpdated);
+}
+
+IPCRecorderWidget::~IPCRecorderWidget() = default;
+
+void IPCRecorderWidget::OnEmulationStarting() {
+    Clear();
+    id_offset = 1;
+
+    // Update the enabled status when the system is powered on.
+    SetEnabled(ui->enabled->isChecked());
+}
+
+QString IPCRecorderWidget::GetStatusStr(const IPCDebugger::RequestRecord& record) const {
+    switch (record.status) {
+    case IPCDebugger::RequestStatus::Invalid:
+        return tr("Invalid");
+    case IPCDebugger::RequestStatus::Sent:
+        return tr("Sent");
+    case IPCDebugger::RequestStatus::Handling:
+        return tr("Handling");
+    case IPCDebugger::RequestStatus::Handled:
+        if (record.translated_reply_cmdbuf[1] == RESULT_SUCCESS.raw) {
+            return tr("Success");
+        }
+        return tr("Error");
+    case IPCDebugger::RequestStatus::HLEUnimplemented:
+        return tr("HLE Unimplemented");
+    default:
+        UNREACHABLE();
+    }
+}
+
+void IPCRecorderWidget::OnEntryUpdated(IPCDebugger::RequestRecord record) {
+    if (record.id < id_offset) { // The record has already been deleted by 'Clear'
+        return;
+    }
+
+    QString service = GetServiceName(record);
+    if (record.status == IPCDebugger::RequestStatus::Handling ||
+        record.status == IPCDebugger::RequestStatus::Handled ||
+        record.status == IPCDebugger::RequestStatus::HLEUnimplemented) {
+
+        service = QStringLiteral("%1 (%2)").arg(service, record.is_hle ? tr("HLE") : tr("LLE"));
+    }
+
+    QTreeWidgetItem item{
+        {QString::number(record.id), GetStatusStr(record), service, GetFunctionName(record)}};
+
+    const int row_id = record.id - id_offset;
+    if (ui->main->invisibleRootItem()->childCount() > row_id) {
+        records[row_id] = record;
+        (*ui->main->invisibleRootItem()->child(row_id)) = item;
+    } else {
+        records.emplace_back(record);
+        ui->main->invisibleRootItem()->addChild(new QTreeWidgetItem(item));
+    }
+
+    if (record.status == IPCDebugger::RequestStatus::HLEUnimplemented ||
+        (record.status == IPCDebugger::RequestStatus::Handled &&
+         record.translated_reply_cmdbuf[1] != RESULT_SUCCESS.raw)) { // Unimplemented / Error
+
+        auto* item = ui->main->invisibleRootItem()->child(row_id);
+        for (int column = 0; column < item->columnCount(); ++column) {
+            item->setBackgroundColor(column, QColor::fromRgb(255, 0, 0));
+        }
+    }
+
+    ApplyFilter(row_id);
+}
+
+void IPCRecorderWidget::SetEnabled(bool enabled) {
+    if (!Core::System::GetInstance().IsPoweredOn()) {
+        return;
+    }
+
+    auto& ipc_recorder = Core::System::GetInstance().Kernel().GetIPCRecorder();
+    ipc_recorder.SetEnabled(enabled);
+
+    if (enabled) {
+        handle = ipc_recorder.BindCallback(
+            [this](const IPCDebugger::RequestRecord& record) { emit EntryUpdated(record); });
+    } else if (handle) {
+        ipc_recorder.UnbindCallback(handle);
+    }
+}
+
+void IPCRecorderWidget::Clear() {
+    id_offset = records.size() + 1;
+
+    records.clear();
+    ui->main->invisibleRootItem()->takeChildren();
+}
+
+QString IPCRecorderWidget::GetServiceName(const IPCDebugger::RequestRecord& record) const {
+    if (Core::System::GetInstance().IsPoweredOn() && record.client_port.id != -1) {
+        const auto service_name =
+            Core::System::GetInstance().ServiceManager().GetServiceNameByPortId(
+                static_cast<u32>(record.client_port.id));
+
+        if (!service_name.empty()) {
+            return QString::fromStdString(service_name);
+        }
+    }
+
+    // Get a similar result from the server session name
+    std::string session_name = record.server_session.name;
+    session_name = Common::ReplaceAll(session_name, "_Server", "");
+    session_name = Common::ReplaceAll(session_name, "_Client", "");
+    return QString::fromStdString(session_name);
+}
+
+QString IPCRecorderWidget::GetFunctionName(const IPCDebugger::RequestRecord& record) const {
+    if (record.untranslated_request_cmdbuf.empty()) { // Cmdbuf is not yet available
+        return tr("Unknown");
+    }
+    const QString header_code =
+        QStringLiteral("0x%1").arg(record.untranslated_request_cmdbuf[0], 8, 16, QLatin1Char('0'));
+    if (record.function_name.empty()) {
+        return header_code;
+    }
+    return QStringLiteral("%1 (%2)").arg(QString::fromStdString(record.function_name), header_code);
+}
+
+void IPCRecorderWidget::ApplyFilter(int index) {
+    auto* item = ui->main->invisibleRootItem()->child(index);
+    const QString filter = ui->filter->text();
+    if (filter.isEmpty()) {
+        item->setHidden(false);
+        return;
+    }
+
+    for (int i = 0; i < item->columnCount(); ++i) {
+        if (item->text(i).contains(filter)) {
+            item->setHidden(false);
+            return;
+        }
+    }
+
+    item->setHidden(true);
+}
+
+void IPCRecorderWidget::ApplyFilterToAll() {
+    for (int i = 0; i < ui->main->invisibleRootItem()->childCount(); ++i) {
+        ApplyFilter(i);
+    }
+}
+
+void IPCRecorderWidget::OpenRecordDialog(QTreeWidgetItem* item, [[maybe_unused]] int column) {
+    int index = ui->main->invisibleRootItem()->indexOfChild(item);
+
+    RecordDialog dialog(this, records[static_cast<std::size_t>(index)], item->text(2),
+                        item->text(3));
+    dialog.exec();
+}
diff --git a/src/citra_qt/debugger/ipc/recorder.h b/src/citra_qt/debugger/ipc/recorder.h
new file mode 100644
index 000000000..63b190790
--- /dev/null
+++ b/src/citra_qt/debugger/ipc/recorder.h
@@ -0,0 +1,52 @@
+// Copyright 2019 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <unordered_map>
+#include <QDockWidget>
+#include "core/hle/kernel/ipc_debugger/recorder.h"
+
+class QTreeWidgetItem;
+
+namespace Ui {
+class IPCRecorder;
+}
+
+class IPCRecorderWidget : public QDockWidget {
+    Q_OBJECT
+
+public:
+    explicit IPCRecorderWidget(QWidget* parent = nullptr);
+    ~IPCRecorderWidget();
+
+    void OnEmulationStarting();
+
+signals:
+    void EntryUpdated(IPCDebugger::RequestRecord record);
+
+private:
+    QString GetStatusStr(const IPCDebugger::RequestRecord& record) const;
+    void OnEntryUpdated(IPCDebugger::RequestRecord record);
+    void SetEnabled(bool enabled);
+    void Clear();
+    void ApplyFilter(int index);
+    void ApplyFilterToAll();
+    QString GetServiceName(const IPCDebugger::RequestRecord& record) const;
+    QString GetFunctionName(const IPCDebugger::RequestRecord& record) const;
+    void OpenRecordDialog(QTreeWidgetItem* item, int column);
+
+    std::unique_ptr<Ui::IPCRecorder> ui;
+    IPCDebugger::CallbackHandle handle;
+
+    // The offset between record id and row id, assuming record ids are assigned
+    // continuously and only the 'Clear' action can be performed, this is enough.
+    // The initial value is 1, which means record 1 = row 0.
+    int id_offset = 1;
+    std::vector<IPCDebugger::RequestRecord> records;
+};
+
+Q_DECLARE_METATYPE(IPCDebugger::RequestRecord);
diff --git a/src/citra_qt/debugger/ipc/recorder.ui b/src/citra_qt/debugger/ipc/recorder.ui
new file mode 100644
index 000000000..78b988fdc
--- /dev/null
+++ b/src/citra_qt/debugger/ipc/recorder.ui
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>IPCRecorder</class>
+ <widget class="QDockWidget" name="IPCRecorder">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>IPC Recorder</string>
+  </property>
+  <widget class="QWidget">
+   <layout class="QVBoxLayout">
+    <item>
+     <widget class="QCheckBox" name="enabled">
+      <property name="text">
+       <string>Enable Recording</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <layout class="QHBoxLayout">
+      <item>
+       <widget class="QLabel">
+        <property name="text">
+         <string>Filter:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLineEdit" name="filter">
+        <property name="placeholderText">
+         <string>Leave empty to disable filtering</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+    <item>
+     <widget class="QTreeWidget" name="main">
+      <property name="alternatingRowColors">
+       <bool>true</bool>
+      </property>
+      <column>
+       <property name="text">
+        <string>#</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Status</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Service</string>
+       </property>
+      </column>
+      <column>
+       <property name="text">
+        <string>Function</string>
+       </property>
+      </column>
+     </widget>
+    </item>
+    <item>
+     <layout class="QHBoxLayout">
+      <item>
+       <spacer>
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QPushButton" name="clearButton">
+        <property name="text">
+         <string>Clear</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index d1a31fa5c..49e4588ae 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -36,6 +36,7 @@
 #include "citra_qt/debugger/graphics/graphics_surface.h"
 #include "citra_qt/debugger/graphics/graphics_tracing.h"
 #include "citra_qt/debugger/graphics/graphics_vertex_shader.h"
+#include "citra_qt/debugger/ipc/recorder.h"
 #include "citra_qt/debugger/lle_service_modules.h"
 #include "citra_qt/debugger/profiler.h"
 #include "citra_qt/debugger/registers.h"
@@ -328,6 +329,13 @@ void GMainWindow::InitializeDebugWidgets() {
             [this] { lleServiceModulesWidget->setDisabled(true); });
     connect(this, &GMainWindow::EmulationStopping, waitTreeWidget,
             [this] { lleServiceModulesWidget->setDisabled(false); });
+
+    ipcRecorderWidget = new IPCRecorderWidget(this);
+    addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget);
+    ipcRecorderWidget->hide();
+    debug_menu->addAction(ipcRecorderWidget->toggleViewAction());
+    connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget,
+            &IPCRecorderWidget::OnEmulationStarting);
 }
 
 void GMainWindow::InitializeRecentFileMenuActions() {
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 780589141..c5861cb2e 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -30,6 +30,7 @@ class GraphicsBreakPointsWidget;
 class GraphicsTracingWidget;
 class GraphicsVertexShaderWidget;
 class GRenderWindow;
+class IPCRecorderWidget;
 class LLEServiceModulesWidget;
 class MicroProfileDialog;
 class MultiplayerState;
@@ -247,6 +248,7 @@ private:
     GraphicsBreakPointsWidget* graphicsBreakpointsWidget;
     GraphicsVertexShaderWidget* graphicsVertexShaderWidget;
     GraphicsTracingWidget* graphicsTracingWidget;
+    IPCRecorderWidget* ipcRecorderWidget;
     LLEServiceModulesWidget* lleServiceModulesWidget;
     WaitTreeWidget* waitTreeWidget;
     Updater* updater;