diff --git a/lib/librte_eal/rte_eal_exports.def b/lib/librte_eal/rte_eal_exports.def
index dd8adfa23..8d8300645 100644
--- a/lib/librte_eal/rte_eal_exports.def
+++ b/lib/librte_eal/rte_eal_exports.def
@@ -11,6 +11,8 @@ EXPORTS
 	rte_devargs_next
 	rte_devargs_parse
 	rte_devargs_remove
+	rte_eal_alarm_set
+	rte_eal_alarm_cancel
 	rte_eal_has_hugepages
 	rte_eal_has_pci
 	rte_eal_init
diff --git a/lib/librte_eal/windows/eal_alarm.c b/lib/librte_eal/windows/eal_alarm.c
new file mode 100644
index 000000000..3b262793a
--- /dev/null
+++ b/lib/librte_eal/windows/eal_alarm.c
@@ -0,0 +1,219 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (c) 2020 Dmitry Kozlyuk
+ */
+
+#include <stdatomic.h>
+#include <stdbool.h>
+
+#include <rte_alarm.h>
+#include <rte_spinlock.h>
+
+#include <rte_eal_trace.h>
+
+#include "eal_windows.h"
+
+enum alarm_state {
+	ALARM_ARMED,
+	ALARM_TRIGGERED,
+	ALARM_CANCELLED
+};
+
+struct alarm_entry {
+	LIST_ENTRY(alarm_entry) next;
+	rte_eal_alarm_callback cb_fn;
+	void *cb_arg;
+	HANDLE timer;
+	atomic_uint state;
+};
+
+static LIST_HEAD(alarm_list, alarm_entry) alarm_list = LIST_HEAD_INITIALIZER();
+
+static rte_spinlock_t alarm_lock = RTE_SPINLOCK_INITIALIZER;
+
+static int intr_thread_exec(void (*func)(void *arg), void *arg);
+
+static void
+alarm_remove_unsafe(struct alarm_entry *ap)
+{
+	LIST_REMOVE(ap, next);
+	CloseHandle(ap->timer);
+	free(ap);
+}
+
+static void
+alarm_callback(void *arg, DWORD low __rte_unused, DWORD high __rte_unused)
+{
+	struct alarm_entry *ap = arg;
+	unsigned int state = ALARM_ARMED;
+
+	if (!atomic_compare_exchange_strong(
+			&ap->state, &state, ALARM_TRIGGERED))
+		return;
+
+	ap->cb_fn(ap->cb_arg);
+
+	rte_spinlock_lock(&alarm_lock);
+	alarm_remove_unsafe(ap);
+	rte_spinlock_unlock(&alarm_lock);
+}
+
+struct alarm_task {
+	struct alarm_entry *entry;
+	LARGE_INTEGER deadline;
+	int ret;
+};
+
+static void
+alarm_set(void *arg)
+{
+	struct alarm_task *task = arg;
+
+	BOOL ret = SetWaitableTimer(
+		task->entry->timer, &task->deadline,
+		0, alarm_callback, task->entry, FALSE);
+	task->ret = ret ? 0 : (-1);
+}
+
+int
+rte_eal_alarm_set(uint64_t us, rte_eal_alarm_callback cb_fn, void *cb_arg)
+{
+	struct alarm_entry *ap;
+	HANDLE timer;
+	FILETIME ft;
+	struct alarm_task task;
+	int ret;
+
+	/* Calculate deadline ASAP, unit of measure = 100ns. */
+	GetSystemTimePreciseAsFileTime(&ft);
+	task.deadline.LowPart = ft.dwLowDateTime;
+	task.deadline.HighPart = ft.dwHighDateTime;
+	task.deadline.QuadPart += 10 * us;
+
+	ap = calloc(1, sizeof(*ap));
+	if (ap == NULL) {
+		RTE_LOG(ERR, EAL, "Cannot allocate alarm entry\n");
+		ret = -ENOMEM;
+		goto exit;
+	}
+
+	timer = CreateWaitableTimer(NULL, FALSE, NULL);
+	if (timer == NULL) {
+		RTE_LOG_WIN32_ERR("CreateWaitableTimer()");
+		ret = -EINVAL;
+		goto fail;
+	}
+
+	ap->timer = timer;
+	ap->cb_fn = cb_fn;
+	ap->cb_arg = cb_arg;
+	task.entry = ap;
+
+	/* Waitable timer must be set in the same thread that will
+	 * do an alertable wait for the alarm to trigger, that is,
+	 * in the interrupt thread. Setting can fail, so do it synchronously.
+	 */
+	ret = intr_thread_exec(alarm_set, &task);
+	if (ret < 0) {
+		RTE_LOG(ERR, EAL, "Cannot setup alarm in interrupt thread\n");
+		goto fail;
+	}
+
+	ret = task.ret;
+	if (ret < 0)
+		goto fail;
+
+	rte_spinlock_lock(&alarm_lock);
+	LIST_INSERT_HEAD(&alarm_list, ap, next);
+	rte_spinlock_unlock(&alarm_lock);
+
+	goto exit;
+
+fail:
+	if (timer != NULL)
+		CloseHandle(timer);
+	if (ap != NULL)
+		free(ap);
+
+exit:
+	rte_eal_trace_alarm_set(us, cb_fn, cb_arg, ret);
+	return ret;
+}
+
+static bool
+alarm_matches(const struct alarm_entry *ap,
+	rte_eal_alarm_callback cb_fn, void *cb_arg)
+{
+	bool any_arg = cb_arg == (void *)(-1);
+	return (ap->cb_fn == cb_fn) && (any_arg || ap->cb_arg == cb_arg);
+}
+
+int
+rte_eal_alarm_cancel(rte_eal_alarm_callback cb_fn, void *cb_arg)
+{
+	struct alarm_entry *ap;
+	unsigned int state;
+	int removed;
+	bool executing;
+
+	removed = 0;
+	do {
+		executing = false;
+
+		rte_spinlock_lock(&alarm_lock);
+
+		LIST_FOREACH(ap, &alarm_list, next) {
+			if (!alarm_matches(ap, cb_fn, cb_arg))
+				continue;
+
+			state = ALARM_ARMED;
+			if (atomic_compare_exchange_strong(
+					&ap->state, &state, ALARM_CANCELLED)) {
+				alarm_remove_unsafe(ap);
+				removed++;
+			} else if (state == ALARM_TRIGGERED)
+				executing = true;
+		}
+
+		rte_spinlock_unlock(&alarm_lock);
+	} while (executing);
+
+	rte_eal_trace_alarm_cancel(cb_fn, cb_arg, removed);
+	return removed;
+}
+
+struct intr_task {
+	void (*func)(void *arg);
+	void *arg;
+	rte_spinlock_t lock; /* unlocked at task completion */
+};
+
+static void
+intr_thread_entry(void *arg)
+{
+	struct intr_task *task = arg;
+	task->func(task->arg);
+	rte_spinlock_unlock(&task->lock);
+}
+
+static int
+intr_thread_exec(void (*func)(void *arg), void *arg)
+{
+	struct intr_task task;
+	int ret;
+
+	task.func = func;
+	task.arg = arg;
+	rte_spinlock_init(&task.lock);
+
+	/* Make timers more precise by synchronizing in userspace. */
+	rte_spinlock_lock(&task.lock);
+	ret = eal_intr_thread_schedule(intr_thread_entry, &task);
+	if (ret < 0) {
+		RTE_LOG(ERR, EAL, "Cannot schedule task to interrupt thread\n");
+		return -EINVAL;
+	}
+
+	/* Wait for the task to complete. */
+	rte_spinlock_lock(&task.lock);
+	return 0;
+}
diff --git a/lib/librte_eal/windows/meson.build b/lib/librte_eal/windows/meson.build
index b690bc6b0..3b2faf29e 100644
--- a/lib/librte_eal/windows/meson.build
+++ b/lib/librte_eal/windows/meson.build
@@ -5,6 +5,7 @@ subdir('include')
 
 sources += files(
 	'eal.c',
+	'eal_alarm.c',
 	'eal_debug.c',
 	'eal_file.c',
 	'eal_hugepages.c',
