Skip to content

Why does headless need to be false for Puppeteer to work?

I’m creating a web api that scrapes a given url and sends that back. I am using Puppeteer to do this. I asked this question: Puppeteer not behaving like in Developer Console

and recieved an answer that suggested it would only work if headless was set to be false. I don’t want to be constantly opening up a browser UI i don’t need (I just the need the data!) so I’m looking for why headless has to be false and can I get a fix that lets headless = true.

Here’s my code:

  .get("/*", (req, res) => {
    global.notBaseURL = req.params[0];
    (async () => {
      const browser = await puppet.launch({ headless: false }); // Line of Interest
      const page = await browser.newPage();
      await page.goto(req.params[0], { waitUntil: "networkidle2" }); //this is the url
      title = await page.$eval("title", (el) => el.innerText);


        title: title,
  .listen(PORT, () => console.log(`Listening on ${PORT}`));

This is the page I’m trying to scrape:


The reason it might work in UI mode but not headless is that sites who aggressively fight scraping will detect that you are running in a headless browser.

Some possible workarounds:

Use puppeteer-extra

Found here: Check out their docs for how to use it. It has a couple plugins that might help in getting past headless-mode detection:

  1. puppeteer-extra-plugin-anonymize-ua — anonymizes your User Agent. Note that this might help with getting past headless mode detection, but as you’ll see if you visit it is unlikely to be enough to keep you from being identified as a repeat visitor.
  2. puppeteer-extra-plugin-stealth — this might help win the cat-and-mouse game of not being detected as headless. There are many tricks that are employed to detect headless mode, and as many tricks to evade them.

Run a “real” Chromium instance/UI

It’s possible to run a single browser UI in a manner that let’s you attach puppeteer to that running instance. Here’s an article that explains it:

Essentially you’re starting Chrome or Chromium (or Edge?) from the command line with --remote-debugging-port=9222 (or any old port?) plus other command line switches depending on what environment you’re running it in. Then you use puppeteer to connect to that running instance instead of having it do the default behavior of launching a headless Chromium instance: const browser = await puppeteer.connect({ browserURL: ENDPOINT_URL });. Read the puppeteer docs here for more info:

The ENDPOINT_URL is displayed in the terminal when you launch the browser from the command line with the --remote-debugging-port=9222 option.

This option is going to require some server/ops mojo, so be prepared to do a lot more Stack Overflow searches. 🙂

There are other strategies I’m sure but those are the two I’m most familiar with. Good luck!