diff --git a/core/src/arch/x86_64/interrupts.zig b/core/src/arch/x86_64/interrupts.zig
index 128f5c7..bb8791a 100644
--- a/core/src/arch/x86_64/interrupts.zig
+++ b/core/src/arch/x86_64/interrupts.zig
@@ -139,7 +139,16 @@ fn pageFault(frame: *InterruptStackFrame) void {
 }
 
 export fn interruptEntry(frame: *InterruptStackFrame) callconv(.C) void {
-    debug.print("Caught interrupt {d}\n", .{frame.isr});
+    if (frame.isr >= 32 and frame.isr < 48) {
+        // IRQ
+        const irq_handler = irq_handlers[frame.error_or_irq];
+        if (irq_handler) |handler| {
+            handler(@intCast(frame.error_or_irq), frame);
+        }
+        pic.picEOI(@intCast(frame.error_or_irq));
+        return;
+    }
+
     switch (frame.isr) {
         @intFromEnum(Exceptions.PageFault) => {
             pageFault(frame);
@@ -149,7 +158,7 @@ export fn interruptEntry(frame: *InterruptStackFrame) callconv(.C) void {
         },
         SYSCALL_INTERRUPT => {
             var args = sys.Arguments{ .arg0 = frame.rdi, .arg1 = frame.rsi, .arg2 = frame.rdx, .arg3 = frame.r10, .arg4 = frame.r8, .arg5 = frame.r9 };
-            sys.invokeSyscall(frame.rax, frame, &args, @ptrFromInt(@as(usize, @intFromPtr(&frame.rax))));
+            sys.invokeSyscall(frame.rax, frame, &args, @ptrCast(&frame.rax));
         },
         else => {},
     }
diff --git a/core/src/arch/x86_64/pit.zig b/core/src/arch/x86_64/pit.zig
new file mode 100644
index 0000000..eacb344
--- /dev/null
+++ b/core/src/arch/x86_64/pit.zig
@@ -0,0 +1,28 @@
+const io = @import("ioports.zig");
+const interrupts = @import("interrupts.zig");
+const pic = @import("pic.zig");
+const thread = @import("../../thread.zig");
+
+// Every timer tick is equivalent to 1 millisecond.
+const TIMER_RESOLUTION = 1;
+
+const PIT_CHANNEL_0 = 0x40;
+
+const base_frequency: u64 = 1193182;
+
+pub fn initializePIT() void {
+    const divisor: u16 = @intCast(base_frequency / (TIMER_RESOLUTION * 1000));
+    if (divisor < 100) {
+        @compileError("Timer resolution is too low");
+    }
+
+    io.outb(PIT_CHANNEL_0, @as(u8, @intCast(divisor & 0xFF)));
+    io.outb(0x80, 0); // short delay
+    io.outb(PIT_CHANNEL_0, @as(u8, @intCast((divisor & 0xFF00) >> 8)));
+
+    _ = interrupts.registerIRQ(0, &pitTimerHandler);
+}
+
+pub fn pitTimerHandler(_: u32, regs: *interrupts.InterruptStackFrame) void {
+    thread.preempt(regs);
+}
diff --git a/core/src/arch/x86_64/platform.zig b/core/src/arch/x86_64/platform.zig
index c535282..b7aa53f 100644
--- a/core/src/arch/x86_64/platform.zig
+++ b/core/src/arch/x86_64/platform.zig
@@ -1,6 +1,7 @@
 const gdt = @import("gdt.zig");
 const idt = @import("idt.zig");
 const pic = @import("pic.zig");
+const pit = @import("pit.zig");
 const interrupts = @import("interrupts.zig");
 
 pub const PAGE_SIZE = 4096;
@@ -26,5 +27,5 @@ pub fn platformInit() void {
 pub fn platformEndInit() void {
     pic.remapPIC();
     interrupts.syncInterrupts();
-    interrupts.enableInterrupts();
+    pit.initializePIT();
 }