diff --git a/src/core/core.cpp b/src/core/core.cpp
index 8ac4481ccb..98f8a7dffd 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -5,6 +5,7 @@
 #include "common/common_types.h"
 
 #include "core/core.h"
+#include "core/core_timing.h"
 
 #include "core/settings.h"
 #include "core/arm/disassembler/arm_disasm.h"
@@ -23,7 +24,17 @@ ARM_Interface*     g_sys_core = nullptr;  ///< ARM11 system (OS) core
 
 /// Run the core CPU loop
 void RunLoop(int tight_loop) {
-    g_app_core->Run(tight_loop);
+    // If the current thread is an idle thread, then don't execute instructions,
+    // instead advance to the next event and try to yield to the next thread
+    if (Kernel::IsIdleThread(Kernel::GetCurrentThreadHandle())) {
+        LOG_TRACE(Core_ARM11, "Idling");
+        CoreTiming::Idle();
+        CoreTiming::Advance();
+        HLE::Reschedule(__func__);
+    } else {
+        g_app_core->Run(tight_loop);
+    }
+
     HW::Update();
     if (HLE::g_reschedule) {
         Kernel::Reschedule();
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index e59ed1b57c..ae2c11a1ca 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -124,6 +124,8 @@ bool LoadExec(u32 entry_point) {
 
     // 0x30 is the typical main thread priority I've seen used so far
     g_main_thread = Kernel::SetupMainThread(0x30);
+    // Setup the idle thread
+    Kernel::SetupIdleThread();
 
     return true;
 }
diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp
index d764511462..58fb62e898 100644
--- a/src/core/hle/kernel/thread.cpp
+++ b/src/core/hle/kernel/thread.cpp
@@ -11,6 +11,7 @@
 #include "common/thread_queue_list.h"
 
 #include "core/core.h"
+#include "core/core_timing.h"
 #include "core/hle/hle.h"
 #include "core/hle/kernel/kernel.h"
 #include "core/hle/kernel/thread.h"
@@ -34,6 +35,7 @@ public:
     inline bool IsReady() const { return (status & THREADSTATUS_READY) != 0; }
     inline bool IsWaiting() const { return (status & THREADSTATUS_WAIT) != 0; }
     inline bool IsSuspended() const { return (status & THREADSTATUS_SUSPEND) != 0; }
+    inline bool IsIdle() const { return idle; }
 
     ResultVal<bool> WaitSynchronization() override {
         const bool wait = status != THREADSTATUS_DORMANT;
@@ -69,6 +71,9 @@ public:
     std::vector<Handle> waiting_threads;
 
     std::string name;
+
+    /// Whether this thread is intended to never actually be executed, i.e. always idle
+    bool idle = false;
 };
 
 // Lists all thread ids that aren't deleted/etc.
@@ -444,7 +449,14 @@ ResultCode SetThreadPriority(Handle handle, s32 priority) {
     return RESULT_SUCCESS;
 }
 
-/// Sets up the primary application thread
+Handle SetupIdleThread() {
+    Handle handle;
+    Thread* thread = CreateThread(handle, "idle", 0, THREADPRIO_LOWEST, THREADPROCESSORID_0, 0, 0);
+    thread->idle = true;
+    CallThread(thread);
+    return handle;
+}
+
 Handle SetupMainThread(s32 priority, int stack_size) {
     Handle handle;
 
@@ -497,6 +509,15 @@ void Reschedule() {
         ResumeThreadFromWait(prev->GetHandle());
 }
 
+bool IsIdleThread(Handle handle) {
+    Thread* thread = g_handle_table.Get<Thread>(handle);
+    if (!thread) {
+        LOG_ERROR(Kernel, "Thread not found %u", handle);
+        return false;
+    }
+    return thread->IsIdle();
+}
+
 ResultCode GetThreadId(u32* thread_id, Handle handle) {
     Thread* thread = g_handle_table.Get<Thread>(handle);
     if (thread == nullptr)
diff --git a/src/core/hle/kernel/thread.h b/src/core/hle/kernel/thread.h
index 0e1397cd96..dfe92d162a 100644
--- a/src/core/hle/kernel/thread.h
+++ b/src/core/hle/kernel/thread.h
@@ -104,6 +104,17 @@ ResultVal<u32> GetThreadPriority(const Handle handle);
 /// Set the priority of the thread specified by handle
 ResultCode SetThreadPriority(Handle handle, s32 priority);
 
+/**
+ * Sets up the idle thread, this is a thread that is intended to never execute instructions,
+ * only to advance the timing. It is scheduled when there are no other ready threads in the thread queue
+ * and will try to yield on every call.
+ * @returns The handle of the idle thread
+ */
+Handle SetupIdleThread();
+
+/// Whether the current thread is an idle thread
+bool IsIdleThread(Handle thread);
+
 /// Initialize threading
 void ThreadingInit();