diff --git a/epicardium/api/interrupt-sender.c b/epicardium/api/interrupt-sender.c
index 53568df9c839b019d33e126cb42166a2b5b78566..fad7ed674ef85f7e0553abbfc86ed1cca6af37d5 100644
--- a/epicardium/api/interrupt-sender.c
+++ b/epicardium/api/interrupt-sender.c
@@ -3,49 +3,20 @@
 #include "tmr_utils.h"
 #include <assert.h>
 
-static bool int_enabled[EPIC_INT_NUM];
-
-void api_interrupt_trigger(api_int_id_t id)
-{
-	assert(id < EPIC_INT_NUM);
-
-	if (int_enabled[id]) {
-		while (API_CALL_MEM->int_id != (api_int_id_t)(-1))
-			;
-
-		API_CALL_MEM->int_id = id;
-		TMR_TO_Start(MXC_TMR5, 1, 0);
-	}
-}
-
 void api_interrupt_init(void)
 {
 	API_CALL_MEM->int_id = (-1);
-
-	for (int i = 0; i < EPIC_INT_NUM; i++) {
-		int_enabled[i] = false;
-	}
-
-	/* Reset interrupt is always enabled */
-	int_enabled[EPIC_INT_RESET] = true;
 }
 
-int epic_interrupt_enable(api_int_id_t int_id)
+bool api_interrupt_is_ready(void)
 {
-	if (int_id >= EPIC_INT_NUM) {
-		return -EINVAL;
-	}
-
-	int_enabled[int_id] = true;
-	return 0;
+	return API_CALL_MEM->int_id == (api_int_id_t)(-1);
 }
 
-int epic_interrupt_disable(api_int_id_t int_id)
+void api_interrupt_trigger(api_int_id_t id)
 {
-	if (int_id >= EPIC_INT_NUM || int_id == EPIC_INT_RESET) {
-		return -EINVAL;
-	}
+	assert(API_CALL_MEM->int_id == (api_int_id_t)(-1));
 
-	int_enabled[int_id] = false;
-	return 0;
+	API_CALL_MEM->int_id = id;
+	TMR_TO_Start(MXC_TMR5, 1, 0);
 }
diff --git a/epicardium/api/interrupt-sender.h b/epicardium/api/interrupt-sender.h
index 84fe4428fdbaf00381fa757392f911ebc48cd961..d4924c0995b2ed46474f64155db67db9415e7843 100644
--- a/epicardium/api/interrupt-sender.h
+++ b/epicardium/api/interrupt-sender.h
@@ -2,4 +2,5 @@
 #include "api/common.h"
 
 void api_interrupt_init(void);
+bool api_interrupt_is_ready(void);
 void api_interrupt_trigger(api_int_id_t id);
diff --git a/epicardium/modules/hardware.c b/epicardium/modules/hardware.c
index 358154334f1b7f3a2d508e7348e70999aa67257c..6d118fe73ef59007c4e3cb915afdba004e678d56 100644
--- a/epicardium/modules/hardware.c
+++ b/epicardium/modules/hardware.c
@@ -1,7 +1,6 @@
 #include "epicardium.h"
 
 #include "api/dispatcher.h"
-#include "api/interrupt-sender.h"
 #include "usb/epc_usb.h"
 #include "modules/filesystem.h"
 #include "modules/log.h"
@@ -171,7 +170,7 @@ int hardware_early_init(void)
 	/*
 	 * API Dispatcher & API Interrupts
 	 */
-	api_interrupt_init();
+	interrupt_init();
 	api_dispatcher_init();
 
 	/*
@@ -237,7 +236,7 @@ int hardware_reset(void)
 	/*
 	 * API Dispatcher & API Interrupts
 	 */
-	api_interrupt_init();
+	interrupt_init();
 	api_dispatcher_init();
 
 	/*
diff --git a/epicardium/modules/interrupts.c b/epicardium/modules/interrupts.c
new file mode 100644
index 0000000000000000000000000000000000000000..e33e35769d887fbcc6eafdf79186e7015f88106c
--- /dev/null
+++ b/epicardium/modules/interrupts.c
@@ -0,0 +1,99 @@
+#include "modules/mutex.h"
+#include "epicardium.h"
+#include "api/interrupt-sender.h"
+#include <assert.h>
+
+struct interrupt_priv {
+	/* Whether this interrupt can be triggered */
+	bool int_enabled[EPIC_INT_NUM];
+	/* Whether this interrupt is waiting to be delivered */
+	bool int_pending[EPIC_INT_NUM];
+	/* Whether any interrupts are currently waiting to be triggered */
+	bool has_pending;
+};
+
+static struct interrupt_priv interrupt_data;
+static struct mutex interrupt_mutex;
+
+void interrupt_trigger(api_int_id_t id)
+{
+	assert(id < EPIC_INT_NUM);
+
+	mutex_lock(&interrupt_mutex);
+	if (!interrupt_data.int_enabled[id])
+		goto out;
+
+	while (!api_interrupt_is_ready())
+		;
+
+	api_interrupt_trigger(id);
+out:
+	mutex_unlock(&interrupt_mutex);
+}
+
+/*
+ * This function solely exists because of that one use of interrupts that breaks
+ * the rules:  The RTC ALARM interrupt is triggered from a hardware ISR where
+ * interrupt_trigger_sync() won't work because it needs to lock a mutex.
+ *
+ * DO NOT USE THIS FUNCTION IN ANY NEW CODE.
+ */
+void __attribute__((deprecated)) interrupt_trigger_unsafe(api_int_id_t id)
+{
+	assert(id < EPIC_INT_NUM);
+
+	if (!interrupt_data.int_enabled[id])
+		return;
+
+	while (!api_interrupt_is_ready())
+		;
+
+	api_interrupt_trigger(id);
+}
+
+static void interrupt_set_enabled(api_int_id_t id, bool enabled)
+{
+	assert(id < EPIC_INT_NUM);
+
+	mutex_lock(&interrupt_mutex);
+	interrupt_data.int_enabled[id] = enabled;
+	mutex_unlock(&interrupt_mutex);
+}
+
+void interrupt_init(void)
+{
+	if (interrupt_mutex.name == NULL)
+		mutex_create(&interrupt_mutex);
+
+	api_interrupt_init();
+
+	/* Reset all irqs to disabled */
+	for (size_t i = 0; i < EPIC_INT_NUM; i++) {
+		interrupt_set_enabled(i, false);
+	}
+
+	/* Reset interrupt is always enabled */
+	interrupt_set_enabled(EPIC_INT_RESET, true);
+}
+
+/* Epic-calls {{{ */
+int epic_interrupt_enable(api_int_id_t int_id)
+{
+	if (int_id >= EPIC_INT_NUM) {
+		return -EINVAL;
+	}
+
+	interrupt_set_enabled(int_id, true);
+	return 0;
+}
+
+int epic_interrupt_disable(api_int_id_t int_id)
+{
+	if (int_id >= EPIC_INT_NUM || int_id == EPIC_INT_RESET) {
+		return -EINVAL;
+	}
+
+	interrupt_set_enabled(int_id, false);
+	return 0;
+}
+/* }}} */
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 474b32930f9b4970fdde4cf65c6891f5eead8a1f..548d8563ea1e7ac2943843931e1733af5fc07dd2 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -9,6 +9,7 @@ module_sources = files(
   'gpio.c',
   'hardware.c',
   'hw-lock.c',
+  'interrupts.c',
   'leds.c',
   'lifecycle.c',
   'light_sensor.c',
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 67557ad4f226aa6ad9a116aa0b940b09889ed2a2..534901c5ccb6d82a69b7b314e8f3e85da4916616 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -4,6 +4,7 @@
 #include "FreeRTOS.h"
 #include "gpio.h"
 #include "modules/mutex.h"
+#include "epicardium.h"
 
 #include <stdint.h>
 #include <stdbool.h>
@@ -27,6 +28,13 @@ int hardware_reset(void);
 void vLifecycleTask(void *pvParameters);
 void return_to_menu(void);
 
+/* ---------- Interrupts --------------------------------------------------- */
+void interrupt_init(void);
+void interrupt_trigger(api_int_id_t id);
+void interrupt_trigger_unsafe(api_int_id_t id) __attribute__((deprecated(
+	"interrupt_trigger_unsafe() is racy and only exists for legacy code."
+)));
+
 /* ---------- Serial ------------------------------------------------------- */
 #define SERIAL_READ_BUFFER_SIZE 128
 #define SERIAL_WRITE_STREAM_BUFFER_SIZE 512