Blog / Make Playwright work with Bun

Make Playwright work with Bun

MK
Mateusz Kelner
October 21, 2025
6 min read
Technical Tutorial
Bun Logo Playwright Logo

Using Node.js is so 2020. Bun is the new kid on the block and I love it but it's not fulfilling the promise of being a Node.js drop in replacement just yet. While it's mostly good enough to use for everything from hobby projects to production apps you will run into hiccups once in a while. Trying to use Playwright to connect to an already running browser is one of those.

And it's not a new one. The issue on bun's repo has been open for over a year ago.

Why doesn't it work?

Playwright's chromium.connectOverCDP() uses a WebSocket connection underneath and utilizes the ws library to upgrade an HTTP request to a WebSocket.

Here's the first problem: Bun's HTTP client doesn't support protocol upgrades yet (HTTP → WebSocket) like Node's http does. That breaks manual WebSocket clients such as ws.

Bun usually mitigates by intercepting import 'ws' and providing a polyfill that wraps the platform WebSocket.

Here's the second problem: Playwright bundles ws during build, so there's no import 'ws' left for Bun to intercept.

Result: Bun can't connect and connectOverCDP() fails.

Why hasn't it been fixed yet?

Implementing the upgrade protocol in Bun's HTTP client would fix the issue. Unfortunately, it's far from a one line fix and not a high priority for the Bun team right now.

But wait, if there's a workaround to ws imports and playwright bundles it at build time couldn't we build the workaround into playwright directly when using Bun?

Yes, we could and here's a PR doing exactly that, however the playwright team decided it's best to wait for bun to support upgrades and that's why the PR is closed.

What do we do now?

We turn to one of the most fun features of our package manager - patch.

If you've never heard of patch before, here's a quick overview

Let's say we installed a package from the npm registry and it's useful enough that we don't want to write our own implementation from scratch but it's missing enough that you would wanna modify it.

Normally you would file an issue, discuss it with the maintainers and maybe even file a PR add/tweak the functionality you need. But what if your change was too proprietary for that or you didn't want to go back and forth with the maintainers or they just simply rejected your idea?

You could just go into your node_modules folder and apply the changes you need. But then you would have to manually keep those changes up to date with the latest version of the package. And all your deployments would have to include those changes somehow.

This is exactly what patch is for. It allows you to modify the package's code and then save that change and reapply it after every install. As long as it's present in patchedDependencies in your package.json.

How do I actually do this and what do I patch?

Let's start by initializing a new bun project

terminal
bun init

Now let's add playwright and browserbase

terminal
bun add playwright-core @browserbasehq/sdk

Browserbase is a service that allows you to run browsers in the cloud. I will use it in this example as that's how I originally stumbled onto this issue. But the patch also fixes connecting to a local browser and other providers.

Now let's create a .env file and paste Browserbase's API key and project ID into it.

.env
BROWSERBASE_API_KEY=your_api_key
BROWSERBASE_PROJECT_ID=your_project_id

Finally we can add some sample code to our index.ts file so we will have something to test our patch with.

index.ts
import { chromium } from 'playwright-core';
import Browserbase from "@browserbasehq/sdk";

const bb = new Browserbase({
  apiKey: process.env.BROWSERBASE_API_KEY
});

const session = await bb.sessions.create({
  projectId: process.env.BROWSERBASE_PROJECT_ID!,
});

const browser = await chromium.connectOverCDP(session.connectUrl);

const defaultContext = browser.contexts()[0];
const page = defaultContext?.pages()[0];

await page?.goto('https://www.example.com')

console.log('Page title:', await page?.title())

await page?.close();
await browser.close();

Now comes the fun part. Let's patch playwright

terminal
bun patch playwright-core

Let's open node_modules/playwright-core/lib/utilsBundle.js and change

utilsBundle.js
const ws = require("./utilsBundleImpl").ws;

to

utilsBundle.js
const ws = 'Bun' in globalThis ? require('ws') : require('./utilsBundleImpl').ws;

One more command to run

terminal
bun patch --commit node_modules/playwright-core --patches-dir=.patches

This will create a .patches folder in your project root and add a playwright-core.patch file to it. It should look something like this

.patches/playwright-core.patch
diff --git a/lib/utilsBundle.js b/lib/utilsBundle.js
index 6833ed2edb6e23c0bb3603591adc5839c7e73846..409ff8ee5d7861b75f1db08dc61d9a881880631b 100644
--- a/lib/utilsBundle.js
+++ b/lib/utilsBundle.js
@@ -59,7 +59,7 @@ const ProgramOption = require("./utilsBundleImpl").ProgramOption;
const progress = require("./utilsBundleImpl").progress;
const SocksProxyAgent = require("./utilsBundleImpl").SocksProxyAgent;
const yaml = require("./utilsBundleImpl").yaml;
-const ws = require("./utilsBundleImpl").ws;
+const ws = 'Bun' in globalThis ? require('ws') : require('./utilsBundleImpl').ws;
const wsServer = require("./utilsBundleImpl").wsServer;
const wsReceiver = require("./utilsBundleImpl").wsReceiver;
const wsSender = require("./utilsBundleImpl").wsSender;

You should also be able to see the patch in your package.json as a patchedDependencies entry.

package.json
"patchedDependencies": {
  "playwright-core": ".patches/playwright-core.patch"
}

And we're good to go, we can now run our code with

terminal
bun index.ts

We don't have to worry about manually changing playwright's code after every install as it will happen automatically.

If you want to see the full code, you can find it here.

Have fun and remember to playwright responsibly :)

Share

Contact

Prefer concise emails

X / Twitter
@MKelner
LinkedIn
Mateusz Kelner
GitHub
Wendrowiec13
Typically replies within 5 minutes