
It’s 11:00 PM on a Tuesday. You’re three coffees deep, staring at a terminal window that’s spitting out a wall of cryptic TimeoutError messages.
The task seemed simple enough: "Just grab the latest product pricing from these five sites every hour." You started with a simple fetch request in Node.js, thinking you’d have this wrapped up by lunch. But then you hit the wall. The HTML returned was a skeleton—a desert of empty <div> tags with names like app-root.
Welcome to the world of the modern web, where everything is a Single Page Application (SPA), and nothing is "real" until a massive bundle of JavaScript executes. To get the data you need, you can’t just request a page; you have to render it.
That’s when you realize you need a headless browser. But as many of us have learned the hard way, spinning up a browser is the easy part. Keeping it running in production without losing your mind? That’s a different story.
What is a Headless Browser, anyway?
In the simplest terms, a headless browser is a web browser without a graphical user interface (GUI). It does everything a "normal" browser does—it parses HTML, executes JavaScript, handles cookies, and renders CSS—but it does it all in the background, without a window popping up on your screen.
Think of it as an invisible chauffeur for the web. You give it an address (a URL), and it goes there, interacts with the elements, and reports back what it saw.
Why can't I just use curl or requests?
A decade ago, you could. Back then, servers sent back fully formed HTML. Today, the web is built on frameworks like React, Vue, and Angular. When you visit a site, the server sends a tiny bit of HTML and a giant script. The script then runs in your browser to fetch the actual content.
If you use a basic HTTP client like Python’s requests or JavaScript’s axios, you’re only getting that initial, empty shell. A headless browser, however, waits for the scripts to execute. It allows you to:
Scrape dynamic content: Access data that only appears after a user interaction or an API call.
Automate testing: Ensure your website works across different "screen" sizes and states.
Generate PDFs or Screenshots: Capture exactly how a page looks to a real user.
Submit forms: Interact with complex login flows that require session handling and CSRF tokens.
The Big Three: Choosing Your Automation Engine
If you’ve spent any time in the dev community lately, you know we love our tools. When it comes to headless browsers, three names dominate the conversation.
1. Puppeteer
Maintained by Google, Puppeteer is a Node.js library that provides a high-level API to control Chrome or Chromium. It’s often the go-to for developers because it’s fast, relatively lightweight, and has excellent documentation.
2. Playwright
The "new" kid on the block (built by many of the original Puppeteer authors at Microsoft), Playwright is designed for the modern era. It supports Chromium, Firefox, and WebKit (Safari) with a single API. Its "auto-wait" feature is a godsend for reducing flakiness.
3. Selenium
The veteran. Selenium has been around forever and supports almost every language imaginable (Java, C#, Python, Ruby). However, it’s often slower and more "clunky" than the newer JS-native alternatives.
Getting Your Hands Dirty: A Practical Example
Let’s look at a common scenario. You want to scrape a list of "Trending Topics" from a site that uses infinite scrolling. Here’s how you’d handle that using Playwright in Python.
import asyncio
from playwright.async_api import async_playwright
async def get_trending_data(url):
async with async_playwright() as p:
# Launching the browser (headless=True is the default)
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# Navigate to the site
await page.goto(url)
# Wait for the dynamic content to actually load
# This is where 'requests' would fail!
await page.wait_for_selector('.trending-item')
# Sometimes you need to scroll to trigger lazy loading
await page.mouse.wheel(0, 2000)
await asyncio.sleep(2) # Give it a second to fetch more data
# Extract the structured data
items = await page.query_selector_all('.trending-item-title')
data = []
for item in items:
text = await item.inner_text()
data.append(text)
await browser.close()
return data
# Usage
# trending_list = asyncio.run(get_trending_data("https://example-news-site.com"))
# print(trending_list)Why this code is better than a simple GET request:
Wait for Selector: We aren't just grabbing HTML; we are waiting for the specific DOM element
.trending-itemto exist.Mouse Interaction: We can simulate a user scrolling down to trigger
onScrollevents that load more data.Real JS Execution: Any obfuscation or data loading via WebSockets is handled automatically by the browser engine.
The "Pain Points": What No One Tells You About Headless Browsers
At this point, you might be thinking, "This is great! I'll just build a fleet of these bots."
Yeah... no. I’ve been there, and the "Maintenance Tax" is real. Here are the frustrations you’ll inevitably run into:
1. The Resource Hog
Running a headless browser is expensive. Even without a UI, Chromium is a memory-hungry beast. If you’re trying to run 50 concurrent browser instances on a modest VPS, you’re going to see your CPU spike to 100% and your RAM evaporate.
2. The "Cat and Mouse" Game
Websites don't always want to be scraped. They use sophisticated bot detection like Cloudflare, Akamai, or CAPTCHAs. Suddenly, your headless browser—which identifies itself as "HeadlessChrome" in the User-Agent—gets blocked. You’ll find yourself diving into "stealth" plugins, rotating proxies, and spoofing canvas fingerprints just to stay invisible.
3. Flakiness (The Developer’s Nightmare)
Selectors change. Classes like .price-tag become ._3xK9_ overnight because of a CSS-in-JS update. Your script breaks. You spend half your week updating CSS selectors instead of building features.
4. Docker Dependency Hell
Try deploying a headless browser in a Docker container. You’ll quickly realize you need to install about 50 different Linux shared libraries (libgbm, libnss3, libasound2...) just to get Chromium to launch. Your lightweight 50MB image just became a 1GB monster.
A Smarter Path: Moving from Browsers to Structured Data
As a developer, your goal isn't to manage a browser. Your goal is to get the data.
When the overhead of maintaining a scraping infrastructure starts to outweigh the value of the data, it's time to look for a more pragmatic solution. This is where the concept of "Web Data as an API" comes in.
Instead of writing 100 lines of Playwright code, dealing with proxy rotation, and worrying about memory leaks, imagine if you could just treat a website like a type-safe JSON endpoint.
Introducing ManyPI: The "Easy Button" for Web Data
This is where a tool like ManyPI changes the game. ManyPI is a SaaS designed specifically for developers who are tired of the headless browser grind. It essentially sits on top of these complex automation engines and turns any website into a structured, type-safe API in seconds.
Instead of manually navigating the DOM and hoping your selectors don't break, you can use ManyPI to define the data you want. It handles the browser scaling, the proxy rotation, and the JS execution for you.
What this looks like in practice:
Instead of the 30-line Python script above, your interaction might look as simple as this:
curl -X POST
'https://app.manypi.com/api/scrape/YOUR_API_ENDPOINT_ID'
-H 'Authorization: Bearer YOUR_API_KEY'
-H 'Content-Type: application/json' When Should You Stick to Manual Headless Browsers?
I’m a pragmatist. I won't tell you to use a SaaS for everything. There are times when you should roll your own headless browser setup:
Internal Tools: If you’re automating a private, internal dashboard that never changes, a simple Puppeteer script is fine.
Heavy Interaction: If you need to perform highly specific, multi-step sequences (like complex drag-and-drop actions or testing a proprietary canvas element), you want the control of a local Playwright instance.
Zero Budget, High Time: If you’re a student or hobbyist with more time than money, learning the ins and outs of browser automation is a great skill to have.
When Should You Switch to an API-First Approach?
Scalability: If you need to scrape thousands of pages a day.
Reliability: If your business depends on this data and you can't afford a script breaking every time a dev at the target company pushes a UI update.
Speed of Development: If you’d rather spend your sprint building your product's core features rather than debugging why Chromium won't launch in your CI/CD pipeline.
Best Practices for the Headless Road Ahead
Regardless of whether you use Puppeteer or a service like ManyPI, here are a few "golden rules" for web automation:
Respect
robots.txt: Don't be a jerk. If a site explicitly asks you not to scrape, try to find an official API first.Rate Limit Yourself: Don't hammer a server with 100 requests per second. It’s bad for the site and a sure way to get your IP blacklisted.
Use "Stealth" Wisely: If you are building your own, use libraries like
puppeteer-extra-plugin-stealthto avoid common detection triggers.Expect Failure: Always wrap your browser logic in
try/catchblocks. The web is chaotic; your code should be resilient.
Wrapping Up
Headless browsers are an incredible piece of technology. They’ve unlocked the ability to interact with the modern, JavaScript-heavy web in ways we couldn't have imagined a decade ago.
But as your projects grow, remember that complexity is a debt. Every line of browser-control code you write is a line you have to maintain. Whenever possible, look for ways to abstract that complexity away—whether that’s through robust libraries like Playwright or by moving to a structured data service like ManyPI.
Stop fighting the browser, and start using the data.
Written by
Ole Mai
Founder / ManyPI

