diff --git a/ext/http/01_http.js b/ext/http/01_http.js
index b41c36446a..580ba11666 100644
--- a/ext/http/01_http.js
+++ b/ext/http/01_http.js
@@ -12,45 +12,34 @@ import {
   op_http_shutdown,
   op_http_start,
   op_http_upgrade_websocket,
-  op_http_websocket_accept_header,
   op_http_write,
   op_http_write_headers,
   op_http_write_resource,
 } from "ext:core/ops";
 const {
-  ArrayPrototypeIncludes,
-  ArrayPrototypeMap,
-  ArrayPrototypePush,
   ObjectPrototypeIsPrototypeOf,
   SafeSet,
   SafeSetIterator,
   SetPrototypeAdd,
   SetPrototypeDelete,
-  StringPrototypeCharCodeAt,
   StringPrototypeIncludes,
-  StringPrototypeSplit,
-  StringPrototypeToLowerCase,
-  StringPrototypeToUpperCase,
   Symbol,
   SymbolAsyncIterator,
   TypeError,
   TypedArrayPrototypeGetSymbolToStringTag,
   Uint8Array,
 } = primordials;
-
+import { _ws } from "ext:deno_http/02_websocket.ts";
 import { InnerBody } from "ext:deno_fetch/22_body.js";
-import { Event, setEventTargetData } from "ext:deno_web/02_event.js";
+import { Event } from "ext:deno_web/02_event.js";
 import { BlobPrototype } from "ext:deno_web/09_file.js";
 import {
-  fromInnerResponse,
-  newInnerResponse,
   ResponsePrototype,
   toInnerResponse,
 } from "ext:deno_fetch/23_response.js";
 import {
   fromInnerRequest,
   newInnerRequest,
-  toInnerRequest,
 } from "ext:deno_fetch/23_request.js";
 import { AbortController } from "ext:deno_web/03_abort_signal.js";
 import {
@@ -63,7 +52,6 @@ import {
   _role,
   _server,
   _serverHandleIdleTimeout,
-  createWebSocketBranded,
   SERVER,
   WebSocket,
 } from "ext:deno_websocket/01_websocket.js";
@@ -409,155 +397,6 @@ function createRespondWith(
   };
 }
 
-const _ws = Symbol("[[associated_ws]]");
-const websocketCvf = buildCaseInsensitiveCommaValueFinder("websocket");
-const upgradeCvf = buildCaseInsensitiveCommaValueFinder("upgrade");
-
-function upgradeWebSocket(request, options = {}) {
-  const inner = toInnerRequest(request);
-  const upgrade = request.headers.get("upgrade");
-  const upgradeHasWebSocketOption = upgrade !== null &&
-    websocketCvf(upgrade);
-  if (!upgradeHasWebSocketOption) {
-    throw new TypeError(
-      "Invalid Header: 'upgrade' header must contain 'websocket'",
-    );
-  }
-
-  const connection = request.headers.get("connection");
-  const connectionHasUpgradeOption = connection !== null &&
-    upgradeCvf(connection);
-  if (!connectionHasUpgradeOption) {
-    throw new TypeError(
-      "Invalid Header: 'connection' header must contain 'Upgrade'",
-    );
-  }
-
-  const websocketKey = request.headers.get("sec-websocket-key");
-  if (websocketKey === null) {
-    throw new TypeError(
-      "Invalid Header: 'sec-websocket-key' header must be set",
-    );
-  }
-
-  const accept = op_http_websocket_accept_header(websocketKey);
-
-  const r = newInnerResponse(101);
-  r.headerList = [
-    ["upgrade", "websocket"],
-    ["connection", "Upgrade"],
-    ["sec-websocket-accept", accept],
-  ];
-
-  const protocolsStr = request.headers.get("sec-websocket-protocol") || "";
-  const protocols = StringPrototypeSplit(protocolsStr, ", ");
-  if (protocols && options.protocol) {
-    if (ArrayPrototypeIncludes(protocols, options.protocol)) {
-      ArrayPrototypePush(r.headerList, [
-        "sec-websocket-protocol",
-        options.protocol,
-      ]);
-    } else {
-      throw new TypeError(
-        `Protocol '${options.protocol}' not in the request's protocol list (non negotiable)`,
-      );
-    }
-  }
-
-  const socket = createWebSocketBranded(WebSocket);
-  setEventTargetData(socket);
-  socket[_server] = true;
-  socket[_idleTimeoutDuration] = options.idleTimeout ?? 120;
-  socket[_idleTimeoutTimeout] = null;
-
-  if (inner._wantsUpgrade) {
-    return inner._wantsUpgrade("upgradeWebSocket", r, socket);
-  }
-
-  const response = fromInnerResponse(r, "immutable");
-
-  response[_ws] = socket;
-
-  return { response, socket };
-}
-
-const spaceCharCode = StringPrototypeCharCodeAt(" ", 0);
-const tabCharCode = StringPrototypeCharCodeAt("\t", 0);
-const commaCharCode = StringPrototypeCharCodeAt(",", 0);
-
-/** Builds a case function that can be used to find a case insensitive
- * value in some text that's separated by commas.
- *
- * This is done because it doesn't require any allocations.
- * @param checkText {string} - The text to find. (ex. "websocket")
- */
-function buildCaseInsensitiveCommaValueFinder(checkText) {
-  const charCodes = ArrayPrototypeMap(
-    StringPrototypeSplit(
-      StringPrototypeToLowerCase(checkText),
-      "",
-    ),
-    (c) => [
-      StringPrototypeCharCodeAt(c, 0),
-      StringPrototypeCharCodeAt(StringPrototypeToUpperCase(c), 0),
-    ],
-  );
-  /** @type {number} */
-  let i;
-  /** @type {number} */
-  let char;
-
-  /** @param {string} value */
-  return function (value) {
-    for (i = 0; i < value.length; i++) {
-      char = StringPrototypeCharCodeAt(value, i);
-      skipWhitespace(value);
-
-      if (hasWord(value)) {
-        skipWhitespace(value);
-        if (i === value.length || char === commaCharCode) {
-          return true;
-        }
-      } else {
-        skipUntilComma(value);
-      }
-    }
-
-    return false;
-  };
-
-  /** @param value {string} */
-  function hasWord(value) {
-    for (let j = 0; j < charCodes.length; ++j) {
-      const { 0: cLower, 1: cUpper } = charCodes[j];
-      if (cLower === char || cUpper === char) {
-        char = StringPrototypeCharCodeAt(value, ++i);
-      } else {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  /** @param value {string} */
-  function skipWhitespace(value) {
-    while (char === spaceCharCode || char === tabCharCode) {
-      char = StringPrototypeCharCodeAt(value, ++i);
-    }
-  }
-
-  /** @param value {string} */
-  function skipUntilComma(value) {
-    while (char !== commaCharCode && i < value.length) {
-      char = StringPrototypeCharCodeAt(value, ++i);
-    }
-  }
-}
-
-// Expose this function for unit tests
-internals.buildCaseInsensitiveCommaValueFinder =
-  buildCaseInsensitiveCommaValueFinder;
-
 function serveHttp(conn) {
   internals.warnOnDeprecatedApi(
     "Deno.serveHttp()",
@@ -568,4 +407,4 @@ function serveHttp(conn) {
   return new HttpConn(rid, conn.remoteAddr, conn.localAddr);
 }
 
-export { _ws, HttpConn, serveHttp, upgradeWebSocket };
+export { HttpConn, serveHttp };
diff --git a/ext/http/02_websocket.ts b/ext/http/02_websocket.ts
new file mode 100644
index 0000000000..073929961a
--- /dev/null
+++ b/ext/http/02_websocket.ts
@@ -0,0 +1,185 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { internals, primordials } from "ext:core/mod.js";
+import { op_http_websocket_accept_header } from "ext:core/ops";
+const {
+  ArrayPrototypeIncludes,
+  ArrayPrototypeMap,
+  ArrayPrototypePush,
+  StringPrototypeCharCodeAt,
+  StringPrototypeSplit,
+  StringPrototypeToLowerCase,
+  StringPrototypeToUpperCase,
+  TypeError,
+  Symbol,
+} = primordials;
+import { toInnerRequest } from "ext:deno_fetch/23_request.js";
+import {
+  fromInnerResponse,
+  newInnerResponse,
+} from "ext:deno_fetch/23_response.js";
+import { setEventTargetData } from "ext:deno_web/02_event.js";
+import {
+  _eventLoop,
+  _idleTimeoutDuration,
+  _idleTimeoutTimeout,
+  _protocol,
+  _readyState,
+  _rid,
+  _role,
+  _server,
+  _serverHandleIdleTimeout,
+  createWebSocketBranded,
+  WebSocket,
+} from "ext:deno_websocket/01_websocket.js";
+
+const _ws = Symbol("[[associated_ws]]");
+
+const websocketCvf = buildCaseInsensitiveCommaValueFinder("websocket");
+const upgradeCvf = buildCaseInsensitiveCommaValueFinder("upgrade");
+
+function upgradeWebSocket(request, options = {}) {
+  const inner = toInnerRequest(request);
+  const upgrade = request.headers.get("upgrade");
+  const upgradeHasWebSocketOption = upgrade !== null &&
+    websocketCvf(upgrade);
+  if (!upgradeHasWebSocketOption) {
+    throw new TypeError(
+      "Invalid Header: 'upgrade' header must contain 'websocket'",
+    );
+  }
+
+  const connection = request.headers.get("connection");
+  const connectionHasUpgradeOption = connection !== null &&
+    upgradeCvf(connection);
+  if (!connectionHasUpgradeOption) {
+    throw new TypeError(
+      "Invalid Header: 'connection' header must contain 'Upgrade'",
+    );
+  }
+
+  const websocketKey = request.headers.get("sec-websocket-key");
+  if (websocketKey === null) {
+    throw new TypeError(
+      "Invalid Header: 'sec-websocket-key' header must be set",
+    );
+  }
+
+  const accept = op_http_websocket_accept_header(websocketKey);
+
+  const r = newInnerResponse(101);
+  r.headerList = [
+    ["upgrade", "websocket"],
+    ["connection", "Upgrade"],
+    ["sec-websocket-accept", accept],
+  ];
+
+  const protocolsStr = request.headers.get("sec-websocket-protocol") || "";
+  const protocols = StringPrototypeSplit(protocolsStr, ", ");
+  if (protocols && options.protocol) {
+    if (ArrayPrototypeIncludes(protocols, options.protocol)) {
+      ArrayPrototypePush(r.headerList, [
+        "sec-websocket-protocol",
+        options.protocol,
+      ]);
+    } else {
+      throw new TypeError(
+        `Protocol '${options.protocol}' not in the request's protocol list (non negotiable)`,
+      );
+    }
+  }
+
+  const socket = createWebSocketBranded(WebSocket);
+  setEventTargetData(socket);
+  socket[_server] = true;
+  socket[_idleTimeoutDuration] = options.idleTimeout ?? 120;
+  socket[_idleTimeoutTimeout] = null;
+
+  if (inner._wantsUpgrade) {
+    return inner._wantsUpgrade("upgradeWebSocket", r, socket);
+  }
+
+  const response = fromInnerResponse(r, "immutable");
+
+  response[_ws] = socket;
+
+  return { response, socket };
+}
+
+const spaceCharCode = StringPrototypeCharCodeAt(" ", 0);
+const tabCharCode = StringPrototypeCharCodeAt("\t", 0);
+const commaCharCode = StringPrototypeCharCodeAt(",", 0);
+
+/** Builds a case function that can be used to find a case insensitive
+ * value in some text that's separated by commas.
+ *
+ * This is done because it doesn't require any allocations.
+ * @param checkText {string} - The text to find. (ex. "websocket")
+ */
+function buildCaseInsensitiveCommaValueFinder(checkText) {
+  const charCodes = ArrayPrototypeMap(
+    StringPrototypeSplit(
+      StringPrototypeToLowerCase(checkText),
+      "",
+    ),
+    (c) => [
+      StringPrototypeCharCodeAt(c, 0),
+      StringPrototypeCharCodeAt(StringPrototypeToUpperCase(c), 0),
+    ],
+  );
+  /** @type {number} */
+  let i;
+  /** @type {number} */
+  let char;
+
+  /** @param {string} value */
+  return function (value) {
+    for (i = 0; i < value.length; i++) {
+      char = StringPrototypeCharCodeAt(value, i);
+      skipWhitespace(value);
+
+      if (hasWord(value)) {
+        skipWhitespace(value);
+        if (i === value.length || char === commaCharCode) {
+          return true;
+        }
+      } else {
+        skipUntilComma(value);
+      }
+    }
+
+    return false;
+  };
+
+  /** @param value {string} */
+  function hasWord(value) {
+    for (let j = 0; j < charCodes.length; ++j) {
+      const { 0: cLower, 1: cUpper } = charCodes[j];
+      if (cLower === char || cUpper === char) {
+        char = StringPrototypeCharCodeAt(value, ++i);
+      } else {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /** @param value {string} */
+  function skipWhitespace(value) {
+    while (char === spaceCharCode || char === tabCharCode) {
+      char = StringPrototypeCharCodeAt(value, ++i);
+    }
+  }
+
+  /** @param value {string} */
+  function skipUntilComma(value) {
+    while (char !== commaCharCode && i < value.length) {
+      char = StringPrototypeCharCodeAt(value, ++i);
+    }
+  }
+}
+
+// Expose this function for unit tests
+internals.buildCaseInsensitiveCommaValueFinder =
+  buildCaseInsensitiveCommaValueFinder;
+
+export { _ws, upgradeWebSocket };
diff --git a/ext/http/lib.rs b/ext/http/lib.rs
index df31b9c445..6fc7207bea 100644
--- a/ext/http/lib.rs
+++ b/ext/http/lib.rs
@@ -131,7 +131,7 @@ deno_core::extension!(
     http_next::op_http_close,
     http_next::op_http_cancel,
   ],
-  esm = ["00_serve.js", "01_http.js"],
+  esm = ["00_serve.js", "01_http.js", "02_websocket.ts"],
 );
 
 pub enum HttpSocketAddr {
diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js
index aa52b0c337..96799cb090 100644
--- a/runtime/js/90_deno_ns.js
+++ b/runtime/js/90_deno_ns.js
@@ -15,6 +15,7 @@ import * as net from "ext:deno_net/01_net.js";
 import * as tls from "ext:deno_net/02_tls.js";
 import * as serve from "ext:deno_http/00_serve.js";
 import * as http from "ext:deno_http/01_http.js";
+import * as websocket from "ext:deno_http/02_websocket.ts";
 import * as errors from "ext:runtime/01_errors.js";
 import * as version from "ext:runtime/01_version.ts";
 import * as permissions from "ext:runtime/10_permissions.js";
@@ -227,7 +228,7 @@ const denoNs = {
   serveHttp: http.serveHttp,
   serve: serve.serve,
   resolveDns: net.resolveDns,
-  upgradeWebSocket: http.upgradeWebSocket,
+  upgradeWebSocket: websocket.upgradeWebSocket,
   utime: fs.utime,
   utimeSync: fs.utimeSync,
   kill: process.kill,