Headless vs headed: how to pick a browser mode
Headless vs headed: which should you use? Pick headless for CI tests, headed for debugging, and Xvfb for headed on servers. Learn the setup for each job.
- headless browser
- playwright
- puppeteer
- xvfb
- chrome devtools protocol
On this page
- Headless vs headed Chromium: what actually differs?
- A concrete case: Playwright 1.57
- Window and screen values
- How do you set headless or headed in Playwright and Puppeteer?
- Does headless vs headed change bot detection and scraping?
- How do you run a headed browser on a server with Xvfb?
- How do you connect to a remote browser when headless is not your setting?
- Headless vs headed: which mode fits each job?
- Frequently asked questions
- Is headless or headed better?
- Is headless mode faster than headed?
- Is headless mode harder to detect?
- Can I run headed Chromium in Docker?
- Can I choose headless or headed on a remote browser?
- What should you do next with headless vs headed?
- Sources
Headless vs headed comes down to a window: headless runs Chromium with no visible window and headed shows one. Chromium has more than one headless implementation, and Playwright’s release notes show it has shipped separate binaries per mode since 1.57. Wayfern’s hosted browser runs headed on Xvfb, so mode isn’t a launch option: pick by job, not speed.
A scraper returns the full page on your laptop, then returns something else in a container. A test passes with a window open and fails in CI. If you write scrapers, monitors or tests, this is the headless vs headed split.
After reading, you can pick a mode for your job, set it in Playwright and Puppeteer, run headed on a server with Xvfb, and connect to a remote browser whose mode someone else chose.
Key Takeaways
- Chrome’s headless documentation says headless means three things: no visible window, the separate
chrome-headless-shellbinary, or new headless mode, which shares the full browser.- Both modes speak CDP, so your Playwright or Puppeteer code doesn’t change (see Playwright’s BrowserType API).
- Datacenter versus residential exit, and a sticky session, change outcomes as much as the mode, per Wayfern’s authenticated scraping notes.
- In CI, default to headless and run headed under
xvfb-runonly when a failure needs a window.
Headless vs headed Chromium: what actually differs?
Headless and headed Chromium differ in whether a window exists, and “headless” itself covers three setups with different binaries. Headed mode draws a real window, on a monitor or on a virtual display such as Xvfb.

1. No visible window. This is the plain meaning. Chromium runs the page, scripts and network stack, but has no on-screen window to draw to.
2. The old implementation as its own binary. Chrome for Developers documents chrome-headless-shell as the home of the old headless mode. It is a separate download, not a flag on the full browser.
3. New headless. This is the full Chromium build started with --headless, so it shares code with the headed browser. That’s why pages behave more like they do in a normal window.
4. Headed on a virtual display. The full browser opens a real window, but the “screen” is software on a machine with no monitor. The Xvfb section below covers the setup.
| Setup | Binary | Window | Where you meet it |
|---|---|---|---|
| No-window headless | Depends on the implementation | None | Any headless run |
chrome-headless-shell | Separate binary | None | Playwright and Puppeteer headless runs |
| New headless | Full Chromium build | None on screen | --headless on Chrome |
| Headed on Xvfb | Full Chromium build | Real, on a virtual screen | Servers, containers, hosted browsers |
A concrete case: Playwright 1.57
Playwright’s browsers documentation lists chromium-headless-shell as its own browser download. The Playwright 1.57 release notes then say Playwright switched “from Chromium, to using Chrome for Testing builds” and that “both headed and headless browsers are subject to this.”
A headless test and a headed test can therefore run on different binaries from one config. Check which one you’re on before you compare behavior.
Window and screen values
Window and screen values depend on whether a real window exists. MDN says outerWidth returns the width of the entire browser window, and MDN’s Screen reference describes screen as usually the one on which the current window is being rendered.
With no window, there’s nothing for those numbers to describe except what the implementation chooses to report.
All four setups answer the same protocol, the Chrome DevTools Protocol. Your Playwright or Puppeteer calls don’t change with the mode. The environment around them does.
How do you set headless or headed in Playwright and Puppeteer?
Playwright and Puppeteer both set the mode with a headless launch option, but the flag only decides the mode when your code launches the browser itself.
- Set Playwright’s default in config. Put
headlessunderuse, and override it per run with the CLI flagnpx playwright test --headed, which the Playwright docs on running tests list.
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({ use: { headless: false } }); - Choose the binary in Playwright. Playwright’s browser docs describe a separate
chrome-headless-shellbuild for headless Chromium. Settingchannel: 'chromium'opts into new headless, which runs the full browser. Tutorials that only showlaunch({ headless })skip this choice.
import { chromium } from "playwright";
const shell = await chromium.launch(); // headless shell
const full = await chromium.launch({ channel: "chromium" }); // new headless
const headed = await chromium.launch({ headless: false }); // real window - Set Puppeteer’s
headlessvalue. According to Puppeteer’s headless modes guide, the option acceptstrue(new headless),false(headed) and'shell'(chrome-headless-shell). Older posts useheadless: "new", which is no longer the way to ask for new headless.
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ headless: "shell" });
const context = await browser.createBrowserContext(); Replace createIncognitoBrowserContext() from older examples with createBrowserContext(), the current method name in the Puppeteer API docs.
- Connect to a remote browser when the mode isn’t yours. With
connectOverCDP()orpuppeteer.connect(), the provider already chose the mode andheadlessis ignored. Our hosted Wayfern browser runs headed on a virtual display (Xvfb), andwidthandheighton the /ws URL resize the real window.
const browser = await chromium.connectOverCDP(
"wss://api.wayfern.com/ws?token=LINK_TOKEN&width=1440&height=900",
);
const page = await browser.contexts()[0].newPage(); Success is a page that loads with your window size. For plain CI test runs, use local headless Playwright instead. If you need a hosted browser, see our notes on anti-detect browser sessions over CDP.
Does headless vs headed change bot detection and scraping?
Headless vs headed can change a few signals a site reads, but no primary source shows that either mode is stealthier. The “headless is stealthier” line has no source behind it, so we don’t repeat it.
The same goes for speed ranges: without a current primary source, any number would be invented.
A site can observe three kinds of signal:
- Window and screen values: the size and geometry your page reports about itself.
- Automation flags: MDN says
navigator.webdriver“indicates whether the user agent is controlled by automation”. - Network exit: the IP address the request comes from, such as a datacenter or residential address.
The useful question is which mode produces consistent signals. A consistent setup reports a window size that matches the viewport you chose, and it keeps one exit for the whole flow.
Exit consistency matters as much as the mode. Wayfern’s documentation puts it this way: “A cookie issued to one IP and replayed from another is an easy signal for a site to read.” If your exit changes halfway through a login, switching between headless and headed won’t repair that.
A datacenter exit and a residential exit look different to a site, and a sticky session holds one residential IP for a whole flow. Wayfern’s page on scraping behind logins describes the pattern as: “drive the sign-in over CDP, hold one sticky residential exit, and solve captchas when challenged.”
These are mechanisms, not guarantees. Detection systems change, and no mode, exit or session setting promises a particular result.
To compare modes on your own target:
- Run the same flow in both modes from the same exit.
- Log the window size,
navigator.webdriverand the IP your page sees. - Keep cookies and storage tied to one exit per run.
- Compare the logged values first, then the timings.
Keep the use cases legitimate: public-data collection, monitoring and testing, within each site’s terms and the law.
How do you run a headed browser on a server with Xvfb?
You run a headed browser on a server by giving it a virtual framebuffer: Xvfb is an X server that draws to memory instead of a monitor. The X.Org Xvfb manual page describes it as “an X server that can run on machines with no display hardware and no physical input devices.” It “emulates a dumb framebuffer using virtual memory.”
Chromium draws a real window into that memory. No one sees it, but the browser behaves as if a screen exists.
- Install Xvfb. Use your distribution’s
xvfbpackage, which provides bothXvfband thexvfb-runwrapper. - Launch headed. In your script, set
headless: falseinchromium.launch(). Without a display, a headed launch fails, which is the error you’re about to fix. - Wrap the command.
xvfb-runstarts a display, runs your command and tears the display down afterward. - Set the screen size. Pass
-screen 0 1920x1080x24to set screen 0’s width, height and depth. Skip it and the window geometry comes from defaults you didn’t choose. - Repeat in the container. Make the same wrapper your
ENTRYPOINT, so the container starts its own display before Node runs.
xvfb-run --server-args="-screen 0 1920x1080x24" node scrape.js On a VPS with no monitor, have the script log window.outerWidth and window.outerHeight. You should see values that fit inside the 1920x1080 screen, not zeros. That’s success: a real window on a display nobody can see.
We run Wayfern’s hosted Chromium this way: headed on a virtual display in containers, so it has a real window like a desktop browser (Wayfern’s CDP sessions page). That’s why the mode isn’t your flag to set when you use connectOverCDP() or puppeteer.connect(). Window size is the part you control:
const browser = await chromium.connectOverCDP(
"wss://api.wayfern.com/ws?token=LINK_TOKEN&width=1440&height=900",
); Wayfern’s docs say width and height take whole CSS pixels, 500-3600 by 400-1900. The real window is resized and nothing is emulated, so layout, media queries, outerWidth/outerHeight and screen.* stay consistent.
For the cases where a hosted browser makes sense, see our anti-detect browser sessions guide.
How do you connect to a remote browser when headless is not your setting?
A hosted browser’s mode is not yours to set, so you connect to it over the Chrome DevTools Protocol (CDP) and describe the window and exit on the URL instead. As Wayfern’s CDP sessions page states, Wayfern runs its Chromium headed on a virtual display (Xvfb) in its containers, so there is no headless option to pass.
- Store a link token. Mint one under “Browser links” and export it as
WF_TOKEN. Don’t paste it into source files. - Connect from Playwright. Opening the socket starts the session. As the Wayfern sessions example puts it, “there is no REST call to start one, and no queue to poll.”
import { chromium } from "playwright";
const url = `wss://api.wayfern.com/ws?token=${process.env.WF_TOKEN}` +
"&width=1440&height=900&country=US&session=run42";
const browser = await chromium.connectOverCDP(url);
const page = await browser.contexts()[0].newPage();
await page.goto("https://example.com"); - Or connect from Puppeteer. Pass the same URL as
browserWSEndpoint.
import puppeteer from "puppeteer-core";
const browser = await puppeteer.connect({ browserWSEndpoint: url }); - Wait for the cold start. Expect about 45 to 50 seconds, longer with a residential exit. The first message back is a JSON
connectedmessage with aconnectionId, then plain CDP.
Success looks like a page that loads from the exit you asked for. Wayfern’s docs say an unknown location is refused with close code 4007 instead of falling back, so check that code first if country fails.
The session id holds one residential IP across a flow. Full parameters are in the same Browser API reference.
The honest catch: it’s Chromium family only on linux-x64, per Wayfern’s Browserbase comparison, and it presents a desktop device. If you only run CI tests, a hosted browser is the wrong tool. Use plain headless Playwright. Rates are on the pricing page.
If your job is a remote session with a fixed exit, try a hosted anti-detect browser session over CDP.
Headless vs headed: which mode fits each job?
The right mode depends on the job: headed or UI Mode for local debugging, headless for CI, one fixed mode for scraping, and for a hosted browser it isn’t your decision at all.
| Job | Mode | Why | Caveat |
|---|---|---|---|
| Local debugging | Headed, or Playwright UI Mode and trace viewer | You see the page and replay each step | Your desktop is not the CI machine |
| CI test runs | Headless; headed under xvfb-run only when a failure needs it | A runner needs no display | Window size and rendering can differ from your laptop |
| Scraping and monitoring | Either, the same on every run | Stable signals matter more than the toggle | A changed exit or session changes what the site sees |
| Hosted or remote browser | Set by the provider | You attach over CDP | The headless option does nothing on a browser you didn’t launch |
For local debugging, you don’t always need a window. Playwright’s docs say “UI Mode lets you explore, run, and debug tests with a time travel experience complete with watch mode.”
The same docs describe the Trace Viewer as a tool to “explore recorded Playwright traces after the script has run,” which also works on a failure from CI.
In CI, stay headless by default. When a failure only appears with a window, run the same command under xvfb-run instead of changing the test. A hosted browser adds a network hop and a cost you don’t need there.
Scraping and monitoring split on a different question. Pick one mode, then hold the window size, the exit (datacenter or residential) and the session (sticky or not) fixed between runs. If you’re comparing tools for this job, our guide to web scraping tools sorts them by what they bill.
With a hosted browser, you never choose. Wayfern runs its Chromium headed on a virtual display (Xvfb) in its containers, so connectOverCDP() and puppeteer.connect() attach to a browser that is already running (Wayfern’s CDP sessions page).
You shape that browser through the URL (width, height, country, session), not a launch flag.
Frequently asked questions
Is headless or headed better?
Neither is better in general. Use headless for CI test runs and unattended jobs, where no one watches the window. Use headed for local debugging and for setups where window and screen values should look like a normal desktop browser. Pick by the job, and hold exit and session constant.
Is headless mode faster than headed?
Headless skips drawing the page to a window, so it does less work. We publish no multiplier. Check the Playwright and Chrome documentation, then time your own run.
Is headless mode harder to detect?
No primary source supports that. Mode changes window and screen values, but exit type (datacenter or residential) and a sticky session matter too. No mode guarantees an outcome.
Can I run headed Chromium in Docker?
You can, using a virtual display. Xvfb’s manual page calls Xvfb “an X server that can run on machines with no display hardware”, so a container can run headed Chromium against it.
Can I choose headless or headed on a remote browser?
Not over CDP on Wayfern, which runs headed on Xvfb. connectOverCDP() and puppeteer.connect() have no mode flag. Set width and height on the /ws URL instead (see anti-detect browser sessions).
What should you do next with headless vs headed?
Treat headless vs headed as a setting you choose on purpose, then test it against one real failure.
Today: Read the release notes for your Playwright version and note which binary runs in each mode. Then set headless explicitly in your config, so a changed default can’t flip the mode silently.
This week: Take one flaky or blocked job and reproduce it headed under xvfb-run. Record the exit (datacenter or residential) and the session it used, because those change outcomes as much as the mode does.
If you only run tests in CI, stop there: plain headless Playwright is the right tool.
If the job needs a real window on a virtual display and an exit you choose, connect Playwright or Puppeteer to Wayfern over CDP with connectOverCDP() or puppeteer.connect(). Wayfern has no free tier, so check current rates on the pricing page before you commit.
Sources
Understanding Headless vs. Headed Modes in Playwright: A… · Release notes | Playwright · Anti-Detect Browser Sessions over CDP | Wayfern · Chrome Headless mode · Browsers | Playwright · Xvfb manual page, X.Org · Wayfern API reference · Wayfern API reference. Every other source is linked where the article cites it.
See us more often in Google
One click marks Wayfern as a preferred source in Google, so our posts can show up more often in your Top Stories, AI Mode and AI Overviews.