diff --git a/CMakeLists.txt b/CMakeLists.txt
index eda9acaf..d3ffa15a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -55,6 +55,7 @@ set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limi
                         src/core/kernel/events.cpp src/core/kernel/threads.cpp
                         src/core/kernel/address_arbiter.cpp src/core/kernel/error.cpp
                         src/core/kernel/file_operations.cpp src/core/kernel/directory_operations.cpp
+                        src/core/kernel/idle_thread.cpp
 )
 set(SERVICE_SOURCE_FILES src/core/services/service_manager.cpp src/core/services/apt.cpp src/core/services/hid.cpp
                          src/core/services/fs.cpp src/core/services/gsp_gpu.cpp src/core/services/gsp_lcd.cpp
diff --git a/include/arm_defs.hpp b/include/arm_defs.hpp
index 1d7a9bde..2f167e2f 100644
--- a/include/arm_defs.hpp
+++ b/include/arm_defs.hpp
@@ -1,8 +1,9 @@
 #pragma once
+#include <cstdint>
 
 // Status register definitions
 namespace CPSR {
-    enum : u32 {
+    enum : std::uint32_t {
         // Privilege modes
         UserMode = 16,
         FIQMode = 17,
@@ -26,7 +27,7 @@ namespace CPSR {
 
 namespace FPSCR {
     // FPSCR Flags
-    enum : u32 {
+    enum : std::uint32_t {
         Sign = 1U << 31U, // Negative condition flag
         Zero = 1 << 30,   // Zero condition flag
         Carry = 1 << 29,   // Carry condition flag
diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp
index 118e315d..a2e22803 100644
--- a/include/kernel/kernel.hpp
+++ b/include/kernel/kernel.hpp
@@ -1,5 +1,6 @@
 #pragma once
 #include <array>
+#include <cassert>
 #include <limits>
 #include <string>
 #include <vector>
@@ -19,7 +20,16 @@ class Kernel {
 
 	// The handle number for the next kernel object to be created
 	u32 handleCounter;
-	std::array<Thread, appResourceLimits.maxThreads> threads;
+	// A list of our OS threads, the max number of which depends on the resource limit (hardcoded 32 per process on retail it seems).
+	// We have an extra thread for when no thread is capable of running. This thread is called the "idle thread" in our code
+	// This thread is set up in setupIdleThread and just yields in a loop to see if any other thread has woken up
+	std::array<Thread, appResourceLimits.maxThreads + 1> threads;
+	static constexpr int idleThreadIndex = appResourceLimits.maxThreads;
+	// Our waitlist system uses a bitfield of 64 bits to show which threads are waiting on an object.
+	// That means we can have a maximum of 63 threads + 1 idle thread. This assert should never trigger because the max thread # is 32
+	// But we have it here for safety purposes
+	static_assert(appResourceLimits.maxThreads <= 63, "The waitlist system is built on the premise that <= 63 threads max can be active");
+
 	std::vector<KernelObject> objects;
 	std::vector<Handle> portHandles;
 
@@ -71,6 +81,7 @@ private:
 	s32 getCurrentResourceValue(const KernelObject* limit, u32 resourceName);
 	u32 getMaxForResource(const KernelObject* limit, u32 resourceName);
 	u32 getTLSPointer();
+	void setupIdleThread();
 
 	bool isWaitable(const KernelObject* object);
 
diff --git a/include/memory.hpp b/include/memory.hpp
index b15cac62..0e75f36c 100644
--- a/include/memory.hpp
+++ b/include/memory.hpp
@@ -111,6 +111,7 @@ class Memory {
 		SharedMemoryBlock(0, 0x1000, KernelHandles::HIDSharedMemHandle)  // HID shared memory
  	};
 
+public:
 	static constexpr u32 pageShift = 12;
 	static constexpr u32 pageSize = 1 << pageShift;
 	static constexpr u32 pageMask = pageSize - 1;
@@ -125,6 +126,7 @@ class Memory {
 	static constexpr u32 DSP_CODE_MEMORY_OFFSET = 0_KB;
 	static constexpr u32 DSP_DATA_MEMORY_OFFSET = 256_KB;
 
+private:
 	std::bitset<FCRAM_PAGE_COUNT> usedFCRAMPages;
 	std::optional<u32> findPaddr(u32 size);
 	u64 timeSince3DSEpoch();
diff --git a/src/core/kernel/idle_thread.cpp b/src/core/kernel/idle_thread.cpp
new file mode 100644
index 00000000..4b0a981a
--- /dev/null
+++ b/src/core/kernel/idle_thread.cpp
@@ -0,0 +1,69 @@
+#include <cstring>
+#include "arm_defs.hpp"
+#include "kernel.hpp"
+
+/*
+	This file sets up an idle thread that's meant to run when no other OS thread can run.
+	It simply idles and constantly yields to check if there's any other thread that can run
+	The code for our idle thread looks like this
+
+idle_thread_main:
+	mov r0, #4096            @ Loop counter
+	
+	.loop:
+	nop; nop; nop; nop       @ NOP 4 times to waste some cycles
+	subs r0, #1              @ Decrement counter by 1, go back to looping if loop counter != 0
+	bne .loop
+
+	// Sleep for 0 seconds with the SleepThread SVC, which just yields execution
+	mov r0, #0
+	mov r1, #0
+	svc SleepThread
+
+	b idle_thread_main
+*/
+
+static constexpr u8 idleThreadCode[] = {
+	0x01, 0x0A, 0xA0, 0xE3, // mov r0, #4096
+	0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, // nop (4 times)
+	0x01, 0x00, 0x50, 0xE2,  // subs r0, #1
+	0xF9, 0xFF, 0xFF, 0x1A,  // bne loop
+	0x00, 0x00, 0xA0, 0xE3,  // mov r0, #0
+	0x00, 0x10, 0xA0, 0xE3,  // mov r1, #0
+	0x0A, 0x00, 0x00, 0xEF,  // svc SleepThread
+	0xF4, 0xFF, 0xFF, 0xEA   // b idle_thread_main
+};
+
+// Set up an idle thread to run when no thread is able to run
+void Kernel::setupIdleThread() {
+	Thread& t = threads[idleThreadIndex];
+	constexpr u32 codeAddress = 0xBFC00000;
+
+	// Reserve some memory for the idle thread's code. We map this memory to vaddr BFC00000 which is not userland-accessible
+	// We only allocate 4KB (1 page) because our idle code is pretty small
+	const u32 fcramIndex = mem.allocateSysMemory(Memory::pageSize);
+	auto vaddr = mem.allocateMemory(codeAddress, fcramIndex, Memory::pageSize, true, true, false, true, false, true);
+	if (!vaddr.has_value() || vaddr.value() != codeAddress) {
+		Helpers::panic("Failed to setup idle thread");
+	}
+	
+	// Copy idle thread code to the allocated FCRAM
+	std::memcpy(&mem.getFCRAM()[fcramIndex], idleThreadCode, sizeof(idleThreadCode));
+
+	t.entrypoint = codeAddress;
+	t.gprs[13] = 0; // Set SP & LR to 0 just in case. The idle thread should never access memory, but let's be safe
+	t.gprs[14] = 0;
+	t.gprs[15] = codeAddress;
+	t.cpsr = CPSR::UserMode;
+	t.fpscr = FPSCR::ThreadDefault;
+
+	// Our idle thread should have as low of a priority as possible, because, well, it's an idle thread.
+	// We handle this by giving it a priority of 0xff, which is lower than is actually allowed for user threads
+	// (High priority value = low priority)
+	t.priority = 0xff;
+	t.status = ThreadStatus::Ready;
+
+	// Add idle thread to the list of thread indices
+	threadIndices.push_back(idleThreadIndex);
+	sortThreads();
+}
\ No newline at end of file
diff --git a/src/core/kernel/kernel.cpp b/src/core/kernel/kernel.cpp
index dd6a6e00..1a5184f3 100644
--- a/src/core/kernel/kernel.cpp
+++ b/src/core/kernel/kernel.cpp
@@ -131,6 +131,7 @@ void Kernel::reset() {
 	// which is thankfully not used. Maybe we should prevent this
 	mainThread = makeThread(0, VirtualAddrs::StackTop, 0x30, -2, 0, ThreadStatus::Running);
 	currentThreadIndex = 0;
+	setupIdleThread();
 
 	// Create some of the OS ports
 	srvHandle = makePort("srv:"); // Service manager port