---
title: "Headless vs Headed Playwright: When AI Agents Need a Visible Browser"
description: "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."
canonical: https://callsphere.ai/blog/headless-vs-headed-playwright-when-ai-agents-need-visible-browser
category: "Learn Agentic AI"
tags: ["Playwright", "Headless Browser", "Headed Mode", "Docker", "CI/CD"]
author: "CallSphere Team"
published: 2026-03-18T00:00:00.000Z
updated: 2026-05-06T07:13:45.590Z
---

# 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

```python
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.

```mermaid
flowchart LR
    DEV(["Developer push"])
    PR["Pull request"]
    LINT["Lint plus type check"]
    TEST["Unit and integration"]
    EVAL["LLM eval gate"]
    BUILD["Build container"]
    SCAN["SBOM plus CVE scan"]
    REG[("Registry")]
    STAGE[("Staging deploy
auto")]
    SOAK["Soak test plus
canary metrics"]
    PROD[("Production deploy
manual gate")]
    DEV --> PR --> LINT --> TEST --> EVAL --> BUILD --> SCAN --> REG --> STAGE --> SOAK --> PROD
    style EVAL fill:#4f46e5,stroke:#4338ca,color:#fff
    style SOAK fill:#f59e0b,stroke:#d97706,color:#1f2937
    style PROD fill:#059669,stroke:#047857,color:#fff
```

## When to Use Headless Mode

Headless mode is the default for good reasons:

```python
# 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:

```python
# 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:

```bash
# Set the environment variable to enable the inspector
PWDEBUG=1 python my_agent_script.py
```

```python
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.

## Codegen: Generating Scripts from Manual Interaction

Playwright can record your manual interactions and generate automation code:

```bash
# 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:

```dockerfile
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:

```dockerfile
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

```yaml
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

```yaml
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:

```python
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:

```bash
# 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

---

Source: https://callsphere.ai/blog/headless-vs-headed-playwright-when-ai-agents-need-visible-browser
