Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 074 / 100

Headless vs headed mode: real-world lessons and examples

When to watch the browser.

Expona Admin8 min read

Headless vs headed mode: real-world lessons and examples

Choosing between headless and headed browser modes in Playwright feels like a small decision—until you're debugging a flaky test at 2 AM or waiting 45 minutes for your CI pipeline to complete. We've learned the hard way that these modes serve fundamentally different purposes, and using them correctly saves time, reduces frustration, and keeps your test suite maintainable.

Understanding the modes

In Playwright, headless mode runs the browser without a graphical user interface, while headed mode launches a visible browser window that you can watch in real time. This distinction matters because it affects performance, debugging capability, and resource consumption—all things that compound across a large test suite.

By default, Playwright runs in headless mode. To switch to headed mode, you add a single flag or configuration option. But the real skill isn't flipping the switch—it's knowing when and why to use each mode.

Headless is faster for CI

Continuous integration environments run on lean servers without display hardware. Headless mode removes the overhead of rendering pixels to a screen, which translates to measurable speed gains. We typically see 15–30% faster execution when running the same tests headless compared to headed on CI machines.

A typical e-commerce checkout flow test that takes 8 seconds in headed mode might run in 6 seconds headless. Multiply that across hundreds of tests, and you're looking at minutes saved per CI run. That compounds to hours saved per week across your team.

Headless mode also consumes less memory and CPU, which matters when you're running tests in parallel. If your CI runner has 4 CPU cores, headless tests let you spin up more concurrent browsers per core compared to headed mode.

typescript
import { test } from '@playwright/test';

export const config = {
  use: {
    headless: true, // Default for CI environments
  },
};

test('checkout flow', async ({ page }) => {
  await page.goto('https://shop.example.com');
  await page.click('text=Add to Cart');
  await page.click('text=Proceed to Checkout');
  // This runs ~20% faster in headless mode
});

The performance advantage is real, but it's only half the story. Headless mode is also deterministic—there are no window focus issues, no accidental mouse movements, and no environment-specific quirks that plague headed testing on different machines. This consistency is why headless is the standard for CI pipelines across the industry.

Headed for local debugging

Watching a test execute in headed mode is invaluable when things go wrong. You see exactly where the mouse moves, which elements get focused, and how the page responds to your actions in real time. We've caught subtle timing issues, CSS problems, and unexpected navigation patterns simply by watching the browser execute once.

When a test fails in CI but passes locally, or when you're writing a new test and it's flaking intermittently, headed mode becomes your debugging tool. You can pause the browser, inspect elements with developer tools, and understand the page state at each step. This visual feedback accelerates the debug cycle dramatically compared to reading logs.

Headed mode is also essential when you're working with complex interactions like drag-and-drop, multi-select flows, or modal dialogs. Logs don't tell you if the modal appeared off-screen or if the drag coordinates were slightly off—your eyes do.

typescript
import { test } from '@playwright/test';

// Local debugging configuration
export const config = {
  use: {
    headless: false, // Headed mode for development
  },
};

test('complex form with validation', async ({ page }) => {
  await page.goto('https://app.example.com/form');
  
  // Watch the form render
  await page.fill('input[name="email"]', 'user@example.com');
  
  // See validation happen in real time
  await page.click('button[type="submit"]');
  
  // Check error message appears
  const error = await page.textContent('.error-message');
  console.log('Error:', error);
});

In headed mode, you also have access to Playwright Inspector, which lets you step through tests line by line, inspect the page DOM, and execute arbitrary JavaScript against the current page state. This tooling is primarily useful for headed mode because stepping through headless tests is less informative—you can't see what the page looks like between steps.

The --headed flag

Playwright's command-line interface includes a --headed flag that overrides your configuration file, letting you run headless tests in headed mode without changing code. This is the primary way we switch modes for debugging on the fly.

typescript
// playwright.config.ts
export default {
  use: {
    headless: true, // Default: CI-friendly
  },
};

// Run with: npx playwright test --headed
// This overrides the config and runs tests in headed mode

The flag is simple but powerful. When a test fails in CI, we can check it out locally, run the test with --headed, and immediately see what went wrong. No need to modify configuration, restart processes, or create temporary branches.

You can also combine --headed with other flags for targeted debugging:

typescript
// Run a single test in headed mode
npx playwright test checkout.spec.ts --headed

// Run tests matching a pattern in headed mode
npx playwright test --grep "payment" --headed

// Run with headed mode and stay open after completion
npx playwright test --headed --debug

The --debug flag launches Inspector and pauses execution before the first step, giving you a chance to inspect the page state and step through at your own pace. This combination is the gold standard for debugging complex test failures.

slowMo option to watch actions

Even in headed mode, tests execute quickly—often too quickly to see what's happening. The slowMo option introduces a millisecond delay between each action, giving your eyes time to follow along and understand the flow.

We typically use 500–1000 ms delays when debugging. At 500 ms, you can clearly see each click, type, and navigation. At 1000 ms, even complex interactions become readable. Going slower than 1000 ms usually isn't worth it unless you're documenting the test for team review.

typescript
import { chromium } from '@playwright/test';

// Debugging session with slowMo
const browser = await chromium.launch({
  headless: false,
  slowMo: 500, // 500ms delay between actions
});

const page = await browser.newPage();
await page.goto('https://app.example.com');

// Each action pauses for 500ms, making it easy to follow
await page.click('text=Login');
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');

// Observe the results before closing
await page.waitForNavigation();
console.log('Login succeeded');

await browser.close();

You can also set slowMo in your configuration file for a consistent debugging experience:

typescript
export default {
  use: {
    headless: false,
    slowMo: 500, // Makes local testing easier to follow
  },
};

slowMo is not just for watching—it's also a diagnostic tool. If a test passes at 500 ms delay but fails at normal speed, you likely have a timing or race condition. The slowMo flag helps you uncover these issues and fix them properly instead of adding arbitrary waits.

One warning: never commit slowMo settings to your CI configuration. Slowing down every test in your CI pipeline defeats the purpose of continuous integration. Reserve slowMo for local debugging and temporary troubleshooting.

Common mistakes

Running headed mode in CI: Some teams default their config to headed mode to match local behavior, then disable it only for CI. This is backwards. The default should always be headless, and you only switch to headed locally or via command-line flags.

Ignoring slowMo during debugging: We've seen developers spend an hour tracing logs instead of running a test with slowMo enabled. Watching a test execute at half speed often reveals the problem immediately.

Assuming headed mode catches all issues: Headed and headless can behave differently due to timing, focus handling, and GPU rendering. A test that passes headed might fail headless, or vice versa. Run your CI in the same mode you intend to use in production (usually headless).

Leaving slowMo enabled in test code: If you hardcode slowMo in your test file instead of using environment variables or CLI flags, it'll slow down your entire test suite. Use configuration or flags instead.

Not using --debug with --headed: These flags work together powerfully. Combining them gives you paused execution, full Inspector access, and visual feedback—the ultimate debugging environment.

Key takeaways

  • Headless mode is your CI default: it's faster, more reliable, and uses fewer resources. Configure your test runner to use headless for automation pipelines.
  • Headed mode is for local development and debugging: when a test fails or you're writing something new, flip to headed mode to see what's happening visually.
  • Use the --headed CLI flag to override configuration without changing code. This is the fastest way to debug a failing test.
  • Enable slowMo when debugging headed tests to slow execution to human-readable speeds. 500 ms is a good starting point.
  • Never commit headed-mode configuration or slowMo settings to your CI pipeline. Reserve these for local use.
  • Combine --headed and --debug flags for the most powerful debugging experience: paused execution, full Inspector tooling, and visual feedback.
  • Remember that headed and headless can behave differently. Always run your final validation tests in the same mode as your production environment (typically headless).
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles