playwright.config.ts sets the test timeout, retry count, parallel worker count, trace behavior, and base URL for every test in the suite. The defaults from npm init playwright@latest work locally but have two CI gotchas: workers: undefined causes resource contention on shared runners, and missing forbidOnly: !!process.env.CI lets a committed test.only() silently skip the entire suite. This guide covers every option you'll use in a real project and ends with a production-ready config that handles authentication setup, environment-specific URLs, and separate reporters for local and CI.

The Minimal Config

After npm init playwright@latest, you get something like:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
  ],
});

Let's go through every section.

Top-Level Options

testDir

Where Playwright looks for test files.

testDir: './tests',

Can be a relative or absolute path. Files matching /.spec.ts or /.test.ts inside this folder are picked up automatically.

fullyParallel

fullyParallel: true,

When true: all tests run in parallel across workers — even tests in the same file.

When false (default before): tests within a file run sequentially, but different files run in parallel.

For most projects: set to true. Tests should be independent enough to run in any order.

forbidOnly

forbidOnly: !!process.env.CI,

If a test has test.only(), this causes the entire run to fail. The pattern !!process.env.CI means it only enforces this in CI — so you can use test.only() locally while debugging, but can't accidentally commit it.

Always use this in production configs.

retries

retries: process.env.CI ? 2 : 0,

How many times to retry a failed test before marking it as failed. In CI: retry twice (helps with flaky tests). Locally: no retries (so you see failures immediately).

If a test fails on first attempt but passes on retry, Playwright marks it as "flaky" in the report.

workers

workers: process.env.CI ? 1 : undefined,

How many parallel workers (browser instances) to run.

  • undefined (default): uses 50% of CPU cores
  • 1: runs all tests sequentially — useful in CI to avoid resource contention
  • 4: runs 4 workers in parallel

For local development with a fast machine, undefined is fine. For CI, 1 is safer unless you have a powerful runner.

timeout

timeout: 30_000,  // 30 seconds per test

Maximum time a single test can run before it's marked as failed. Default is 30 seconds. For slow tests or slow CI machines, increase to 60 seconds.

expect.timeout

expect: {
  timeout: 5_000,  // 5 seconds for each expect() assertion
},

Playwright assertions are auto-retrying — they keep trying until the condition is met or this timeout expires. Default is 5 seconds.

The use Block (Shared Test Options)

use contains options that apply to all tests (unless overridden by a project).

use: {
  baseURL: 'http://localhost:3000',
  
  // Browser settings
  headless: true,
  viewport: { width: 1280, height: 720 },
  
  // Recording
  trace: 'on-first-retry',     // 'on-first-retry' | 'retain-on-failure' | 'always' | 'off'
  video: 'retain-on-failure',  // 'retain-on-failure' | 'always' | 'off'
  screenshot: 'only-on-failure',
  
  // Timing
  actionTimeout: 10_000,       // Timeout for each action (click, fill, etc.)
  navigationTimeout: 30_000,   // Timeout for page.goto() and page.waitForURL()
  
  // HTTP
  ignoreHTTPSErrors: true,     // Useful for staging environments with self-signed certs
  
  // Locale
  locale: 'en-US',
  timezoneId: 'America/New_York',
},

baseURL

When set, page.goto('/login') resolves to http://localhost:3000/login. Makes tests portable across environments.

Use environment variables:

baseURL: process.env.BASE_URL || 'http://localhost:3000',

trace

Playwright traces capture a full recording of every action (DOM snapshot, screenshots, network calls) for debugging. Options:

  • 'on-first-retry' — capture on the first retry of a failed test ✅ Recommended
  • 'retain-on-failure' — capture on any failed test (more storage)
  • 'always' — capture everything (lots of storage, slow)
  • 'off' — no traces

View traces with npx playwright show-trace trace.zip.

video

  • 'retain-on-failure' — save video only for failed tests ✅ Recommended
  • 'always' — save video for everything
  • 'off' — no video

projects — Multiple Browser Configs

Projects let you run the same tests across different browsers, viewports, or configurations.

projects: [
  // Desktop browsers
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'] },
  },
  {
    name: 'firefox',
    use: { ...devices['Desktop Firefox'] },
  },
  {
    name: 'webkit',
    use: { ...devices['Desktop Safari'] },
  },
  
  // Mobile
  {
    name: 'mobile-chrome',
    use: { ...devices['Pixel 7'] },
  },
  {
    name: 'mobile-safari',
    use: { ...devices['iPhone 14'] },
  },
],

devices['Desktop Chrome'] is a preset that sets viewport, user agent, and other browser-specific defaults. Full list: npx playwright show-report or the Playwright docs.

Projects for different environments

projects: [
  {
    name: 'staging',
    use: {
      ...devices['Desktop Chrome'],
      baseURL: 'https://staging.myapp.com',
    },
    testMatch: '**/*.spec.ts',
  },
  {
    name: 'production',
    use: {
      ...devices['Desktop Chrome'],
      baseURL: 'https://myapp.com',
    },
    testMatch: '**/smoke/*.spec.ts',  // Only smoke tests in production
  },
],

Setup projects (global setup)

projects: [
  {
    name: 'setup',
    testMatch: /.*\.setup\.ts/,  // Files like auth.setup.ts
  },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'] },
    dependencies: ['setup'],  // Runs setup first
  },
],

Used to run authentication setup once before all tests, then reuse the saved auth state.

reporter

What format to generate test results in:

reporter: [
  ['html'],                                // HTML report in playwright-report/
  ['junit', { outputFile: 'results.xml' }], // JUnit XML for CI
  ['list'],                                 // Live console output
],

For local development: 'html' is great — open playwright-report/index.html for a full visual report.

For CI: add 'junit' so CI tools (GitHub Actions, GitLab CI, Jenkins) can parse results.

globalSetup and globalTeardown

For setup that runs once before the entire test suite starts:

globalSetup: './global-setup.ts',
globalTeardown: './global-teardown.ts',

// global-setup.ts
import { chromium } from '@playwright/test';

export default async function globalSetup() {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  // Log in and save auth state
  await page.goto('http://localhost:3000/login');
  await page.fill('[data-testid="email"]', 'admin@test.com');
  await page.fill('[data-testid="password"]', 'AdminPass1');
  await page.click('[data-testid="submit"]');
  await page.context().storageState({ path: 'auth.json' });
  
  await browser.close();
}

Then tests use the saved auth state without logging in again:

use: {
  storageState: 'auth.json',
},

A Production-Ready Config

import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';

dotenv.config();

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  
  timeout: 45_000,
  expect: { timeout: 8_000 },
  
  reporter: [
    ['html'],
    ...(process.env.CI ? [['junit', { outputFile: 'test-results/results.xml' }] as const] : []),
  ],
  
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    video: 'retain-on-failure',
    screenshot: 'only-on-failure',
    actionTimeout: 10_000,
    navigationTimeout: 30_000,
  },
  
  projects: [
    {
      name: 'setup',
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
    {
      name: 'api',
      testMatch: '**/api/**/*.spec.ts',
      use: { storageState: undefined },  // API tests don't need browser auth
    },
  ],
});

Running Specific Projects

# All tests on chromium
npx playwright test --project=chromium

# Only the setup project
npx playwright test --project=setup

# Multiple projects
npx playwright test --project=chromium --project=firefox

Summary of Most Important Options

| Option | Recommendation |

|--------|---------------|

| testDir | './tests' |

| fullyParallel | true |

| forbidOnly | !!process.env.CI |

| retries | CI ? 2 : 0 |

| timeout | 30_000–60_000 |

| trace | 'on-first-retry' |

| video | 'retain-on-failure' |

| baseURL | From environment variable |

| reporter | HTML locally, + JUnit in CI |

The config file is worth spending 30 minutes to get right at the start of a project. A well-configured playwright.config.ts saves hours of debugging and CI troubleshooting later.

→ See also: Installing Playwright: Step-by-Step Setup Guide (2026) | Playwright Environment Configuration: Local, Staging, and Production | Parallel Execution in Playwright: Workers, Shards, and Sharding for Speed | Environment Variables in Playwright Tests: A Complete Guide