# Use Puppeteer with an anti-detect browser

> Use Puppeteer with an anti-detect browser through puppeteer.connect: start the profile, pass its wsEndpoint, set defaultViewport to null, then disconnect.

Source: https://mosaicbrowser.com/guides/use-puppeteer-with-an-anti-detect-browser

## How to use Puppeteer with an anti-detect browser

To use Puppeteer with an anti-detect browser, start the profile through the browser's API and call puppeteer.connect with the browserWSEndpoint it returns. Pass defaultViewport: null so Puppeteer does not shrink every page to 800x600, and call disconnect, not close, when the script ends.

![puppeteer.connect, not puppeteer.launch, beside a screenshot: mosaic API keys: one per script or tool, each revocable on its own](https://mosaicbrowser.com/covers/use-puppeteer-with-an-anti-detect-browser.webp)

Puppeteer was written to launch Chrome, and most of its defaults assume it did. When an anti-detect browser launches the profile instead, three of those defaults work against you: the viewport, the way the script ends, and how text gets typed. To use Puppeteer with an anti-detect browser you connect instead of launching, and the connection itself is one call.

## How to use Puppeteer with an anti-detect browser: connect, do not launch

`puppeteer.launch()` starts a fresh Chrome with none of the profile in it. `puppeteer.connect()` attaches to a browser that is already running, and takes either `browserWSEndpoint` (the DevTools WebSocket address) or `browserURL` (the HTTP address of the debugging port) ([Puppeteer docs](https://pptr.dev/api/puppeteer.connectoptions), read 2 November 2026).

In Mosaic, `POST /profiles/:id/start` on the [local REST API](https://mosaicbrowser.com/features/rest-api) launches the profile with its fingerprint, proxy and cookies, and returns `wsEndpoint` and `debugPort`. Either works with Puppeteer:

```
import puppeteer from 'puppeteer-core'

const BASE = 'http://127.0.0.1:7317/api/v1'
const headers = { Authorization: `Bearer ${process.env.MOSAIC_KEY}` }

const started = await fetch(`${BASE}/profiles/${id}/start`, {
  method: 'POST', headers
}).then((r) => r.json())

const browser = await puppeteer.connect({
  browserWSEndpoint: started.wsEndpoint,
  defaultViewport: null
})

const [page] = await browser.pages()
await page.goto('https://example.com/orders')

await browser.disconnect()
await fetch(`${BASE}/profiles/${id}/stop`, { method: 'POST', headers })

```

If the profile has a proxy and the proxy does not answer, the start call returns a `502` that names it, and no browser opens. Check the response status before connecting.

### browserWSEndpoint or browserURL?

Either reaches the same browser. `wsEndpoint` from the start call is the socket itself, so Puppeteer connects in one step. `browserURL` is `http://127.0.0.1:` plus the `debugPort` from the same reply, and Puppeteer asks that address for the socket before it connects. The second is handy when a different process attaches later and only knows the port: `GET /profiles/:id/status` returns both for a running profile, and answers with an idle record rather than a 404 when the profile is not running, so a script can poll it without special cases.

Starting a profile that is already running returns the existing session instead of opening a second browser. A script that crashed halfway can call start again and reconnect.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: REST API](https://mosaicbrowser.com/features/rest-api)

## defaultViewport: null is not optional

Puppeteer's `ConnectOptions` give `defaultViewport` a default of `{width: 800, height: 600}`, set on each page. On a plain test browser that is a convenience. On a profile it changes `innerWidth` and `innerHeight` to 800 by 600 inside a window that is still the size the profile claims, so the page can read a window and a viewport that disagree. Passing `null` leaves the real size alone. The screen and window values come from [real hardware records](https://mosaicbrowser.com/features/real-fingerprints), and they only stay consistent if nothing redraws them.

The same reasoning covers `page.setUserAgent`, `page.emulateTimezone` and the stealth plugins that rewrite `navigator` properties in JavaScript. Mosaic changes those values in C++ inside its engine, so a page, its iframes and its workers all read one machine. A second layer in JavaScript gives a page two answers to compare.

## End with disconnect, then stop

The two Puppeteer calls do different things. `browser.close()` "Closes this browser and all associated pages" ([Puppeteer docs](https://pptr.dev/api/puppeteer.browser.close), read 2 November 2026). `browser.disconnect()` "Disconnects Puppeteer from this browser, but leaves the process running" ([Puppeteer docs](https://pptr.dev/api/puppeteer.browser.disconnect), read 2 November 2026).

Use `disconnect`, then `POST /profiles/:id/stop`. Mosaic's stop goes over DevTools so Chromium runs its unload handlers and flushes the cookie and storage databases before it exits, and it falls back to a signal only if that does not finish. The script started nothing, so it should close nothing; the browser's life belongs to whatever launched it.

![The Mosaic API screen with three named API keys, each revocable on its own](https://mosaicbrowser.com/shots/api-keys.png)
*One API key per script, so each can be revoked alone*

## Typing: delay is a constant

`page.keyboard.type('text', { delay: 100 })` sends a keydown, keypress or input, and keyup for each character, and the `delay` is "the time to wait between keydown and keyup in milliseconds" ([Puppeteer docs](https://pptr.dev/api/puppeteer.keyboard.type), read 2 November 2026). Every key is held for the same 100 ms. People do not type like that: the gaps between keys are uneven, longer after punctuation, and broken by the odd mistake.

Mosaic's `POST /profiles/:id/page/type` types into a field with right-skewed gaps, slower punctuation and shifted characters, occasional pauses, and a typo now and then that is backspaced and corrected. Speed defaults to 320 characters a minute. You can call it in the middle of a Puppeteer script, because Chrome has accepted more than one DevTools client at a time since Chrome 63 ([CDP documentation](https://chromedevtools.github.io/devtools-protocol/), read 2 November 2026).

## What Puppeteer turns on when it attaches

Puppeteer, like Playwright, relies on the DevTools command `Runtime.Enable`, and the rebrowser project documents how a page can detect it ([rebrowser-patches](https://github.com/rebrowser/rebrowser-patches), read 2 November 2026). On our engine we measured that alone flipping deviceandbrowserinfo from `isBot=false` to `isBot=true`; `Page.enable`, `Runtime.evaluate` and `Input.dispatchKeyEvent` did not. For a page that checks, Mosaic's page routes read the page as an accessibility tree and click and type from an isolated world, without ever enabling `Runtime`.

The steps are the same for Playwright, with its own traps, in [using Playwright with an anti-detect browser](https://mosaicbrowser.com/guides/use-playwright-with-an-anti-detect-browser). The REST API is part of Pro, $49 a month with a 14-day free trial, on the [pricing page](https://mosaicbrowser.com/pricing).

## Sources

1. [Puppeteer docs: ConnectOptions](https://pptr.dev/api/puppeteer.connectoptions), read November 2, 2026
2. [Puppeteer docs: Browser.disconnect()](https://pptr.dev/api/puppeteer.browser.disconnect), read November 2, 2026
3. [Puppeteer docs: Browser.close()](https://pptr.dev/api/puppeteer.browser.close), read November 2, 2026
4. [Puppeteer docs: Keyboard.type()](https://pptr.dev/api/puppeteer.keyboard.type), read November 2, 2026
5. [rebrowser-patches: Runtime.Enable leak (GitHub)](https://github.com/rebrowser/rebrowser-patches), read November 2, 2026
6. [Chrome DevTools Protocol documentation](https://chromedevtools.github.io/devtools-protocol/), read November 2, 2026

## Questions

### Should I install puppeteer or puppeteer-core?

Our examples import puppeteer-core, because the browser already exists: Mosaic starts it. You only need the library that talks the protocol.

### Why are my pages 800x600 after connecting?

Puppeteer's ConnectOptions set a default viewport of 800 by 600 on each page. Pass defaultViewport: null to keep the profile's real window size, which is part of its fingerprint.

### Can I use a stealth plugin as well?

On Mosaic we advise against it. The profile's values are already changed inside the engine; a plugin that patches them again in JavaScript gives a page two answers to compare.

### Does browser.close() stop the Mosaic profile?

It shuts the browser down from the script. Mosaic notices the exit, but the profile was started by Mosaic and is better stopped by it: call browser.disconnect(), which leaves the process running, then stop the profile through the API, which closes it over DevTools and falls back to a signal only if that does not finish.

## Related

[How to use Playwright with an anti-detect browser](https://mosaicbrowser.com/guides/use-playwright-with-an-anti-detect-browser): Use Playwright with an anti-detect browser by starting the profile first and attaching with connectOverCDP. Keep its context; leave the fingerprint alone.

[How to automate multiple browser profiles](https://mosaicbrowser.com/guides/automate-multiple-browser-profiles): Automate multiple browser profiles by giving each account a persistent profile, starting each from a script and attaching Playwright or Puppeteer over CDP.

[Anti-detect browser REST API: profiles from your scripts](https://mosaicbrowser.com/features/rest-api): Mosaic's anti-detect browser REST API runs on 127.0.0.1 with API keys. Create and start profiles, then attach Puppeteer or Playwright over CDP.

[Machines that exist: fingerprints drawn from real hardware](https://mosaicbrowser.com/features/real-fingerprints): Mosaic draws each profile's GPU, screen, CPU, memory and fonts from one real device, so the parts of a fingerprint agree with each other and with the host.









## Run this guide on your own accounts

Every Mosaic profile is a fingerprint, a proxy exit and a cookie jar that agree with each other, kept on your computer. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
