diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml
index 05f74b6378c6..568828b17bee 100644
--- a/.github/workflows/win.yml
+++ b/.github/workflows/win.yml
@@ -7,6 +7,7 @@ concurrency:
   cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
 
 env:
+  SPEC_SPLIT_DOTS: 160
   CI_LLVM_VERSION: "18.1.1"
 
 jobs:
@@ -266,13 +267,47 @@ jobs:
         run: make -f Makefile.win samples
 
   x86_64-windows-release:
-    if: github.repository_owner == 'crystal-lang' && (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/ci/'))
     needs: [x86_64-windows-libs, x86_64-windows-dlls, x86_64-windows-llvm-libs, x86_64-windows-llvm-dlls]
     uses: ./.github/workflows/win_build_portable.yml
     with:
       release: true
       llvm_version: "18.1.1"
 
+  x86_64-windows-test-interpreter:
+    runs-on: windows-2022
+    needs: [x86_64-windows-release]
+    steps:
+      - name: Disable CRLF line ending substitution
+        run: |
+          git config --global core.autocrlf false
+
+      - name: Download Crystal source
+        uses: actions/checkout@v4
+
+      - name: Download Crystal executable
+        uses: actions/download-artifact@v4
+        with:
+          name: crystal-release
+          path: build
+
+      - name: Restore LLVM
+        uses: actions/cache/restore@v4
+        with:
+          path: llvm
+          key: llvm-libs-${{ env.CI_LLVM_VERSION }}-msvc
+          fail-on-cache-miss: true
+
+      - name: Set up environment
+        run: |
+          Add-Content $env:GITHUB_PATH "$(pwd)\build"
+          Add-Content $env:GITHUB_ENV "CRYSTAL_SPEC_COMPILER_BIN=$(pwd)\build\crystal.exe"
+
+      - name: Run stdlib specs with interpreter
+        run: bin\crystal i spec\std_spec.cr
+
+      - name: Run primitives specs with interpreter
+        run: bin\crystal i spec\primitives_spec.cr
+
   x86_64-windows-installer:
     if: github.repository_owner == 'crystal-lang' && (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/ci/'))
     runs-on: windows-2022
diff --git a/.github/workflows/win_build_portable.yml b/.github/workflows/win_build_portable.yml
index d2ed6469d264..12ee17da9e68 100644
--- a/.github/workflows/win_build_portable.yml
+++ b/.github/workflows/win_build_portable.yml
@@ -114,7 +114,7 @@ jobs:
       - name: Build Crystal
         run: |
           bin/crystal.bat env
-          make -f Makefile.win -B ${{ inputs.release && 'release=1' || '' }}
+          make -f Makefile.win -B ${{ inputs.release && 'release=1' || '' }} interpreter=1
 
       - name: Download shards release
         uses: actions/checkout@v4
diff --git a/spec/std/http/client/client_spec.cr b/spec/std/http/client/client_spec.cr
index 451960a8c79f..6bd04ab3e2f2 100644
--- a/spec/std/http/client/client_spec.cr
+++ b/spec/std/http/client/client_spec.cr
@@ -6,6 +6,12 @@ require "http/server"
 require "http/log"
 require "log/spec"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending HTTP::Client
+  {% skip_file %}
+{% end %}
+
 private def test_server(host, port, read_time = 0.seconds, content_type = "text/plain", write_response = true, &)
   server = TCPServer.new(host, port)
   begin
diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr
index 5e1e5dab76f6..3980084ea414 100644
--- a/spec/std/http/server/server_spec.cr
+++ b/spec/std/http/server/server_spec.cr
@@ -4,6 +4,12 @@ require "http/client"
 require "../../../support/ssl"
 require "../../../support/channel"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending HTTP::Server
+  {% skip_file %}
+{% end %}
+
 # TODO: replace with `HTTP::Client.get` once it supports connecting to Unix socket (#2735)
 private def unix_request(path)
   UNIXSocket.open(path) do |io|
diff --git a/spec/std/http/web_socket_spec.cr b/spec/std/http/web_socket_spec.cr
index 75a54e91fb2e..164a1d067df5 100644
--- a/spec/std/http/web_socket_spec.cr
+++ b/spec/std/http/web_socket_spec.cr
@@ -7,6 +7,12 @@ require "../../support/fibers"
 require "../../support/ssl"
 require "../socket/spec_helper.cr"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending HTTP::WebSocket
+  {% skip_file %}
+{% end %}
+
 private def assert_text_packet(packet, size, final = false)
   assert_packet packet, HTTP::WebSocket::Protocol::Opcode::TEXT, size, final: final
 end
diff --git a/spec/std/io/io_spec.cr b/spec/std/io/io_spec.cr
index 6974a9fe3466..c584ec81a1e8 100644
--- a/spec/std/io/io_spec.cr
+++ b/spec/std/io/io_spec.cr
@@ -816,23 +816,26 @@ describe IO do
           io.gets_to_end.should eq("\r\nFoo\nBar")
         end
 
-        it "gets ascii from socket (#9056)" do
-          server = TCPServer.new "localhost", 0
-          sock = TCPSocket.new "localhost", server.local_address.port
-          begin
-            sock.set_encoding("ascii")
-            spawn do
-              client = server.accept
-              message = client.gets
-              client << "#{message}\n"
+        # TODO: Windows networking in the interpreter requires #12495
+        {% unless flag?(:interpreted) || flag?(:win32) %}
+          it "gets ascii from socket (#9056)" do
+            server = TCPServer.new "localhost", 0
+            sock = TCPSocket.new "localhost", server.local_address.port
+            begin
+              sock.set_encoding("ascii")
+              spawn do
+                client = server.accept
+                message = client.gets
+                client << "#{message}\n"
+              end
+              sock << "K\n"
+              sock.gets.should eq("K")
+            ensure
+              server.close
+              sock.close
             end
-            sock << "K\n"
-            sock.gets.should eq("K")
-          ensure
-            server.close
-            sock.close
           end
-        end
+        {% end %}
       end
 
       describe "encode" do
diff --git a/spec/std/oauth2/client_spec.cr b/spec/std/oauth2/client_spec.cr
index 3ee66e29ab49..ee445f3426e7 100644
--- a/spec/std/oauth2/client_spec.cr
+++ b/spec/std/oauth2/client_spec.cr
@@ -3,6 +3,12 @@ require "oauth2"
 require "http/server"
 require "../http/spec_helper"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending OAuth2::Client
+  {% skip_file %}
+{% end %}
+
 describe OAuth2::Client do
   describe "authorization uri" do
     it "gets with default endpoint" do
diff --git a/spec/std/openssl/ssl/server_spec.cr b/spec/std/openssl/ssl/server_spec.cr
index 2e0e413a618d..8618ed780a50 100644
--- a/spec/std/openssl/ssl/server_spec.cr
+++ b/spec/std/openssl/ssl/server_spec.cr
@@ -3,6 +3,12 @@ require "socket"
 require "../../spec_helper"
 require "../../../support/ssl"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending OpenSSL::SSL::Server
+  {% skip_file %}
+{% end %}
+
 describe OpenSSL::SSL::Server do
   it "sync_close" do
     TCPServer.open(0) do |tcp_server|
diff --git a/spec/std/openssl/ssl/socket_spec.cr b/spec/std/openssl/ssl/socket_spec.cr
index bbc5b11e4b9b..47374ce28cca 100644
--- a/spec/std/openssl/ssl/socket_spec.cr
+++ b/spec/std/openssl/ssl/socket_spec.cr
@@ -4,6 +4,12 @@ require "../../spec_helper"
 require "../../socket/spec_helper"
 require "../../../support/ssl"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending OpenSSL::SSL::Socket
+  {% skip_file %}
+{% end %}
+
 describe OpenSSL::SSL::Socket do
   describe OpenSSL::SSL::Socket::Server do
     it "auto accept client by default" do
diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr
index 57f90121c26b..d41ee0bed242 100644
--- a/spec/std/process_spec.cr
+++ b/spec/std/process_spec.cr
@@ -55,7 +55,12 @@ private def newline
 end
 
 # interpreted code doesn't receive SIGCHLD for `#wait` to work (#12241)
-pending_interpreted describe: Process do
+{% if flag?(:interpreted) && !flag?(:win32) %}
+  pending Process
+  {% skip_file %}
+{% end %}
+
+describe Process do
   describe ".new" do
     it "raises if command doesn't exist" do
       expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do
diff --git a/spec/std/socket/socket_spec.cr b/spec/std/socket/socket_spec.cr
index 2127e196b746..98555937dea3 100644
--- a/spec/std/socket/socket_spec.cr
+++ b/spec/std/socket/socket_spec.cr
@@ -2,6 +2,12 @@ require "./spec_helper"
 require "../../support/tempfile"
 require "../../support/win32"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending Socket
+  {% skip_file %}
+{% end %}
+
 describe Socket, tags: "network" do
   describe ".unix" do
     it "creates a unix socket" do
diff --git a/spec/std/socket/tcp_socket_spec.cr b/spec/std/socket/tcp_socket_spec.cr
index 68c00ccd2e79..f3d460f92401 100644
--- a/spec/std/socket/tcp_socket_spec.cr
+++ b/spec/std/socket/tcp_socket_spec.cr
@@ -3,6 +3,12 @@
 require "./spec_helper"
 require "../../support/win32"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending TCPSocket
+  {% skip_file %}
+{% end %}
+
 describe TCPSocket, tags: "network" do
   describe "#connect" do
     each_ip_family do |family, address|
diff --git a/spec/std/socket/unix_server_spec.cr b/spec/std/socket/unix_server_spec.cr
index ca364f08667c..60f0279b4091 100644
--- a/spec/std/socket/unix_server_spec.cr
+++ b/spec/std/socket/unix_server_spec.cr
@@ -4,6 +4,12 @@ require "../../support/fibers"
 require "../../support/channel"
 require "../../support/tempfile"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending UNIXServer
+  {% skip_file %}
+{% end %}
+
 describe UNIXServer do
   describe ".new" do
     it "raises when path is too long" do
diff --git a/spec/std/socket/unix_socket_spec.cr b/spec/std/socket/unix_socket_spec.cr
index 24777bada67f..c51f37193c0e 100644
--- a/spec/std/socket/unix_socket_spec.cr
+++ b/spec/std/socket/unix_socket_spec.cr
@@ -2,6 +2,12 @@ require "spec"
 require "socket"
 require "../../support/tempfile"
 
+# TODO: Windows networking in the interpreter requires #12495
+{% if flag?(:interpreted) && flag?(:win32) %}
+  pending UNIXSocket
+  {% skip_file %}
+{% end %}
+
 describe UNIXSocket do
   it "raises when path is too long" do
     with_tempfile("unix_socket-too_long-#{("a" * 2048)}.sock") do |path|
diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr
index 48cc3351a3c6..5d7e627031f0 100644
--- a/spec/std/uuid_spec.cr
+++ b/spec/std/uuid_spec.cr
@@ -1,6 +1,7 @@
 require "spec"
 require "uuid"
 require "spec/helpers/string"
+require "../support/wasm32"
 
 describe "UUID" do
   describe "#==" do
diff --git a/src/compiler/crystal/loader/msvc.cr b/src/compiler/crystal/loader/msvc.cr
index 05bf988c9218..772e4c5c232f 100644
--- a/src/compiler/crystal/loader/msvc.cr
+++ b/src/compiler/crystal/loader/msvc.cr
@@ -133,15 +133,25 @@ class Crystal::Loader
   end
 
   def load_file?(path : String | ::Path) : Bool
+    # API sets shouldn't be linked directly from linker flags, but just in case
+    if api_set?(path)
+      return load_dll?(path.to_s)
+    end
+
     return false unless File.file?(path)
 
     # On Windows, each `.lib` import library may reference any number of `.dll`
     # files, whose base names may not match the library's. Thus it is necessary
     # to extract this information from the library archive itself.
-    System::LibraryArchive.imported_dlls(path).each do |dll|
-      dll_full_path = @dll_search_paths.try &.each do |search_path|
-        full_path = File.join(search_path, dll)
-        break full_path if File.file?(full_path)
+    System::LibraryArchive.imported_dlls(path).all? do |dll|
+      # API set names do not refer to physical filenames despite ending with
+      # `.dll`, and therefore should not use a path search:
+      # https://learn.microsoft.com/en-us/cpp/windows/universal-crt-deployment?view=msvc-170#local-deployment
+      unless api_set?(dll)
+        dll_full_path = @dll_search_paths.try &.each do |search_path|
+          full_path = File.join(search_path, dll)
+          break full_path if File.file?(full_path)
+        end
       end
       dll = dll_full_path || dll
 
@@ -152,13 +162,16 @@ class Crystal::Loader
       #
       # Note that the compiler's directory and PATH are effectively searched
       # twice when coming from the interpreter
-      handle = open_library(dll)
-      return false unless handle
-
-      @handles << handle
-      @loaded_libraries << (module_filename(handle) || dll)
+      load_dll?(dll)
     end
+  end
+
+  private def load_dll?(dll)
+    handle = open_library(dll)
+    return false unless handle
 
+    @handles << handle
+    @loaded_libraries << (module_filename(handle) || dll)
     true
   end
 
@@ -190,6 +203,12 @@ class Crystal::Loader
     @handles.clear
   end
 
+  # Returns whether *dll* names an API set according to:
+  # https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-apisets#api-set-contract-names
+  private def api_set?(dll)
+    dll.to_s.matches?(/^(?:api-|ext-)[a-zA-Z0-9-]*l\d+-\d+-\d+\.dll$/)
+  end
+
   private def module_filename(handle)
     Crystal::System.retry_wstr_buffer do |buffer, small_buf|
       len = LibC.GetModuleFileNameW(handle, buffer, buffer.size)
diff --git a/src/kernel.cr b/src/kernel.cr
index 14e66bd4fade..16c4a770309a 100644
--- a/src/kernel.cr
+++ b/src/kernel.cr
@@ -616,3 +616,16 @@ end
     Crystal::System::Signal.setup_default_handlers
   {% end %}
 {% end %}
+
+# This is a temporary workaround to ensure there is always something in the IOCP
+# event loop being awaited, since both the interrupt loop and the fiber stack
+# pool collector are disabled in interpreted code. Without this, asynchronous
+# code that bypasses `Crystal::IOCP::OverlappedOperation` does not currently
+# work, see https://github.com/crystal-lang/crystal/pull/14949#issuecomment-2328314463
+{% if flag?(:interpreted) && flag?(:win32) %}
+  spawn(name: "Interpreter idle loop") do
+    while true
+      sleep 1.day
+    end
+  end
+{% end %}