Headless vs Headed Playwright: When AI Agents Need a Visible Browser
Understand the differences between headless and headed browser modes in Playwright, when to use each for AI agents, and how to configure headed mode in Docker, CI/CD, and remote environments.
Headless vs Headed: What Is the Difference?
A headless browser runs without any visible window. It executes the same browser engine — rendering HTML, executing JavaScript, handling CSS — but does not draw pixels to a screen. A headed browser runs with a visible GUI window where you can see every page load, click, and navigation in real time.
Playwright defaults to headless mode, which is the right choice for production AI agents. But headed mode is invaluable for development, debugging, and specific use cases where visual confirmation is required.
Launching in Each Mode
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# Headless (default) — no visible window
headless_browser = p.chromium.launch(headless=True)
# Headed — visible browser window
headed_browser = p.chromium.launch(headless=False)
# Headed with slow motion — adds delay between actions
debug_browser = p.chromium.launch(
headless=False,
slow_mo=500, # 500ms pause between each action
)
headless_browser.close()
headed_browser.close()
debug_browser.close()
The slow_mo option is particularly useful during development. It slows down every Playwright action so you can visually follow what your agent is doing.
When to Use Headless Mode
Headless mode is the default for good reasons:
# Production scraping agent — headless is faster and uses less memory
browser = p.chromium.launch(headless=True)
# CI/CD pipeline — no display available
browser = p.chromium.launch(headless=True)
# Server-side automation — no GUI needed
browser = p.chromium.launch(headless=True)
# Batch processing — efficiency over visibility
browser = p.chromium.launch(headless=True)
Advantages of headless mode:
- Faster — no rendering overhead for drawing pixels
- Less memory — no GPU memory for rendering
- No display required — works on servers, containers, CI/CD
- More stable — no window management issues
When to Use Headed Mode
Headed mode shines in specific scenarios:
# Debugging a failing automation script
browser = p.chromium.launch(headless=False, slow_mo=1000)
# Sites that detect headless browsers
browser = p.chromium.launch(headless=False)
# User-supervised agent — human watches and can intervene
browser = p.chromium.launch(headless=False)
# Recording a demo or training video
browser = p.chromium.launch(headless=False, slow_mo=300)
Some websites detect headless browsers by checking browser properties, WebGL rendering capabilities, or behavioral patterns. Running headed can bypass these checks.
Using Playwright Inspector for Debugging
Playwright includes a built-in inspector that opens alongside the headed browser:
# Set the environment variable to enable the inspector
PWDEBUG=1 python my_agent_script.py
import os
os.environ["PWDEBUG"] = "1"
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# Inspector opens automatically with PWDEBUG=1
browser = p.chromium.launch(headless=False)
page = browser.new_page()
# Each action pauses, letting you inspect the page state
page.goto("https://example.com")
page.get_by_text("More information").click()
browser.close()
The inspector lets you step through actions one at a time, inspect selectors, and see what the browser sees at each step.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
Codegen: Generating Scripts from Manual Interaction
Playwright can record your manual interactions and generate automation code:
# Open a browser and record interactions
playwright codegen https://example.com
# Generate Python async code
playwright codegen --target python-async https://example.com
# Record to a file
playwright codegen --target python -o my_script.py https://example.com
# Use a specific viewport
playwright codegen --viewport-size=375,812 https://example.com
This is useful for AI agent developers who need to automate a complex workflow. Record the manual steps first, then refine the generated code into your agent logic.
Running Headed Playwright in Docker
Docker containers do not have a display by default. To run headed Playwright in Docker, you need a virtual display:
FROM mcr.microsoft.com/playwright/python:v1.49.0-noble
# Install virtual display
RUN apt-get update && apt-get install -y xvfb
# Copy your application
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
# Run with virtual display
CMD ["xvfb-run", "--auto-servernum", "python", "agent.py"]
For headless-only Docker deployments (the common case), the official Playwright image works without any display setup:
FROM mcr.microsoft.com/playwright/python:v1.49.0-noble
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "agent.py"]
CI/CD Configuration
GitHub Actions
name: Browser Agent Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install playwright pytest
- run: playwright install --with-deps chromium
- run: pytest tests/ --headed=false
GitLab CI
browser_tests:
image: mcr.microsoft.com/playwright/python:v1.49.0-noble
script:
- pip install -r requirements.txt
- pytest tests/
Building a Mode-Switching Agent
A well-designed agent should support both modes, switching based on environment:
import os
from playwright.sync_api import sync_playwright
class BrowserAgent:
def __init__(self, debug: bool = False):
self.debug = debug or os.getenv("AGENT_DEBUG") == "1"
self.slow_mo = 500 if self.debug else 0
def run(self, url: str, task_fn):
with sync_playwright() as p:
browser = p.chromium.launch(
headless=not self.debug,
slow_mo=self.slow_mo,
)
context = browser.new_context(
record_video_dir="./debug_videos/" if self.debug else None,
)
page = context.new_page()
# Enable tracing in debug mode
if self.debug:
context.tracing.start(
screenshots=True,
snapshots=True,
sources=True,
)
try:
result = task_fn(page, url)
return result
except Exception as e:
if self.debug:
page.screenshot(path="error_screenshot.png")
print(f"Error screenshot saved. URL: {page.url}")
raise
finally:
if self.debug:
context.tracing.stop(path="trace.zip")
print("Trace saved to trace.zip")
print("View with: playwright show-trace trace.zip")
context.close()
browser.close()
# Usage
def my_task(page, url):
page.goto(url)
return page.title()
# Production mode
agent = BrowserAgent(debug=False)
title = agent.run("https://example.com", my_task)
# Debug mode
agent = BrowserAgent(debug=True)
title = agent.run("https://example.com", my_task)
Viewing Traces After Headless Runs
Even in headless mode, you can capture traces for post-mortem debugging:
# View the trace file in the Playwright trace viewer
playwright show-trace trace.zip
This opens a web-based viewer where you can step through every action, see screenshots at each step, inspect the DOM, view network requests, and analyze timing — all from a headless run.
FAQ
Does headless mode produce different results than headed mode?
In most cases, no. The browser engine behaves identically in both modes. However, some websites detect headless mode by checking properties like navigator.webdriver, WebGL rendering differences, or missing plugins. If a site works in headed mode but fails in headless, it likely has headless detection. Try removing the webdriver flag with page.add_init_script() or switch to headed mode.
How do I run headed Playwright on a remote server over SSH?
You need X11 forwarding. Connect with ssh -X user@server, then run your script normally. Alternatively, use a VNC server on the remote machine and connect with a VNC client. For most production use cases, capturing traces in headless mode and viewing them locally with playwright show-trace is more practical than streaming the GUI.
Does slow_mo affect the reliability of my tests?
slow_mo adds a fixed delay between every Playwright action, but it does not change the auto-waiting behavior. Your script remains reliable because Playwright still waits for elements to be actionable before interacting with them. slow_mo is purely additive — it will not make flaky tests pass, but it will not make passing tests fail either.
#HeadlessBrowser #PlaywrightDebugging #DockerAutomation #CICD #AIAgents #BrowserTesting #HeadedMode
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.