Skip to content
Learn Agentic AI11 min read0 views

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

Share this article
C

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.