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.
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 cores1: runs all tests sequentially — useful in CI to avoid resource contention4: 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 testMaximum 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=firefoxSummary 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.