Resource library

QA How-To

How to Fix Playwright "Timed out waiting from config.webServer"

Fix Playwright timed out waiting from config.webServer by checking server startup, URL and port, readiness status, Docker networking, and CI timeouts.

22 min read | 3,461 words

TL;DR

Run the server command outside Playwright and probe the exact webServer.url from the test environment. Fix command, host, port, route, or container networking mismatches first; increase webServer.timeout only when the server becomes healthy after a measured slow startup.

Key Takeaways

  • Run the webServer command and curl the exact configured URL before changing timeouts.
  • webServer.timeout controls startup readiness; test and assertion timeouts do not.
  • Keep the server host and port aligned with webServer.url and use.baseURL.
  • Use a real health endpoint and inspect stdout and stderr for startup failures.
  • Disable server reuse in CI and probe from inside the test container when using Docker.
  • Raise the startup budget only after measuring a healthy but slow cold boot.

If you need to fix Playwright Timed out waiting from config.webServer, the failure appears before any test body runs: Playwright launched or checked the configured web server but did not observe its readiness condition before the startup deadline. Start by checking the server command and the exact URL from the same environment that runs the tests.

Error: Timed out waiting 60000ms from config.webServer.

The number reflects the configured webServer.timeout; 60000ms is the documented default. Your output may show another value. This guide gives you a small reproducible setup, then a sequence of checks that separates a dead process, a wrong address, a bad readiness response, and a genuinely slow startup. The relevant configuration is documented in Playwright's web server guide.

TL;DR

Run the server command outside Playwright, probe the exact webServer.url, and only then change webServer.timeout. For a local Vite project, a typical starting point is:

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

export default defineConfig({
  use: { baseURL: 'http://127.0.0.1:4173' },
  webServer: {
    command: 'npm run dev -- --host 127.0.0.1 --port 4173 --strictPort',
    url: 'http://127.0.0.1:4173/',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
    stdout: 'pipe',
    stderr: 'pipe',
  },
});

Use this only if npm run dev really starts Vite and your app serves / at that address. --strictPort prevents Vite from silently moving to a different port. If the URL still fails under curl, increasing the timeout will merely postpone the same error. Keep a single host spelling across the server, readiness URL, and baseURL. If the browser later reports a connection failure after the server was ready, use the separate Playwright connection refused guide.

What the Error Actually Means

Playwright Test evaluates webServer during runner setup. It may reuse an available server, or spawn webServer.command and repeatedly check the readiness target. With url, a response in the documented accepted range of 2xx, 3xx, 400, 401, 402, or 403 counts as ready; redirects are followed. A connection that never succeeds, or a final response outside that range, can keep setup waiting. With the older port property, the runner checks whether the port accepts a connection. url is preferred because it can verify an HTTP endpoint rather than only an open socket.

A startup failure occurs before the test timeout, assertion timeout, or navigation timeout matters. test.setTimeout(), expect(...).toBeVisible({ timeout }), and use.actionTimeout do not extend webServer.timeout. The distinction matters in triage: if the stack trace points at config.webServer, diagnose the process and probe first. If a test starts and then times out, consult the Playwright 30000ms test timeout guide. The official timeout reference documents these separate budgets.

When you specify several web servers, every required entry must become ready before specs begin. Add name to each entry so logs identify the failing service. Also remember that webServer.url and use.baseURL have different jobs: the former gates startup; the latter resolves relative navigation such as page.goto('/'). Keeping them aligned prevents a successful startup check followed by navigation to a different host.

Root-Cause Decision Table

Symptom Root cause Fix
curl cannot connect to the configured URL, but the app logs another port Port or host mismatch Bind an explicit host and port; make url match
npm run dev exits with a script or package error Launch command fails Repair the script and run it independently
Command works in one directory but fails in the test runner Wrong working directory or environment Set cwd and pass required environment variables
Socket accepts connections but the readiness URL returns 404, 500, or 503 Wrong readiness path or unhealthy app Probe a lightweight valid endpoint
Local runs pass, cold CI startup exceeds the deadline Real startup cost Measure boot time, move builds earlier, then set a measured timeout
Local runs pass only while another app already occupies the port Stale server hidden by reuse Disable reuse in CI and identify the listener
Host can reach the app but the test container cannot Docker network boundary Bind the app to 0.0.0.0 and use a reachable service name
One of several services hangs before tests start Multi-service dependency or readiness race Name and probe each service separately

The table is a starting hypothesis, not a diagnosis. Execute the verification command in the relevant section and compare its output with the URL in your config. A green Playwright test after an unexplained timeout can conceal a reused process or a different server, so preserve the process and response evidence.

1. Fix Playwright Timed Out Waiting from config.webServer When the URL or Port Is Wrong

The most common mismatch is simple: the application prints one address while Playwright waits on another. Dev servers may choose a spare port, bind only to a particular interface, or display localhost while the config uses an address that resolves differently in a container. Do not copy a URL from yesterday's terminal. Start the server and read its current listening address.

For Vite, make the port deterministic. The -- passes subsequent arguments through npm to the script, and --strictPort makes a conflict fail visibly instead of selecting another port. Keep the same value in webServer.url and use.baseURL:

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

const origin = 'http://127.0.0.1:4173';
export default defineConfig({
  use: { baseURL: origin },
  webServer: {
    command: 'npm run dev -- --host 127.0.0.1 --port 4173 --strictPort',
    url: `${origin}/`,
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
    stdout: 'pipe',
    stderr: 'pipe',
  },
});

Verify the app command before invoking the runner. In one terminal run npm run dev -- --host 127.0.0.1 --port 4173 --strictPort; in another run:

curl -i --max-time 5 http://127.0.0.1:4173/
npx playwright test --list

curl should show an HTTP response from your app. The second command confirms Playwright finds test files, but it does not prove the server can start; run npx playwright test for that. If curl works only with localhost or an IPv6 address, choose one reachable spelling and use it consistently. A port collision with --strictPort is useful evidence: identify the process rather than accepting a silently shifted port.

2. Fix Playwright Timed Out Waiting from config.webServer When the Command Cannot Stay Running

webServer.command must start a long-lived HTTP process. A script that performs only a build and exits cannot satisfy an HTTP readiness URL. A script name that does not exist, missing dependencies, a syntax error, or an application crash will prevent readiness as well. Read the output from the command, not just the final Playwright line.

For an npm project, inspect the script and execute it from the repository root. The following config exposes both output streams while preserving the app's existing dev script:

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

export default defineConfig({
  webServer: {
    command: 'npm run dev',
    url: 'http://127.0.0.1:4173/',
    stdout: 'pipe',
    stderr: 'pipe',
    timeout: 60_000,
    reuseExistingServer: false,
  },
  use: { baseURL: 'http://127.0.0.1:4173' },
});

This variant assumes the existing dev script binds port 4173. If it does not, correct the script or the URL before using it. To verify a real project, run:

npm run
npm run dev

The first command lists available scripts. The second must remain running and display the serving address, not merely finish with exit code zero. In another terminal, run curl -i http://127.0.0.1:4173/. When the output says Missing script: dev, install failures, or a framework configuration error, fix that underlying failure first. Playwright cannot make a broken application boot. The Playwright TypeScript framework setup guide covers a fuller project layout if the test command itself is still being assembled.

A background shell trick such as npm run dev & inside webServer.command also causes confusing lifecycle behavior. Let Playwright own the foreground server process. It will manage startup and teardown. If the application delegates to another process, ensure the parent remains alive and forwards its output so the readiness check has a process to observe.

3. Correct the Working Directory and Required Environment

Monorepos commonly put playwright.config.ts in one directory and the frontend package in another. Playwright starts the command from the config file's directory unless cwd is set. A command that works after you cd into the app can fail when the runner launches it at the root. The same pattern appears when a local shell has secrets or mode variables that CI lacks.

For a config stored at the repository root with the app also at that root, anchor cwd explicitly and pass a harmless port value through env. This complete example avoids relying on whichever directory invoked npx:

import { defineConfig } from '@playwright/test';
import { fileURLToPath } from 'node:url';

const root = fileURLToPath(new URL('.', import.meta.url));
export default defineConfig({
  webServer: {
    command: 'npm run dev -- --host 127.0.0.1 --port 4173 --strictPort',
    cwd: root,
    env: { ...process.env, PORT: '4173' },
    url: 'http://127.0.0.1:4173/',
    stdout: 'pipe',
    stderr: 'pipe',
  },
  use: { baseURL: 'http://127.0.0.1:4173' },
});

In a monorepo, point cwd to the actual package directory, for example by resolving new URL('./apps/web/', import.meta.url) when that directory exists. Do not assume Vite reads PORT; the CLI arguments in the sample set its listening port. The environment object demonstrates how to supply variables required by an application while inheriting existing values. Avoid printing private values in CI logs.

Verify the working directory independently:

pwd
npm run dev -- --host 127.0.0.1 --port 4173 --strictPort

Run those commands from the directory assigned to cwd, then execute npx playwright test after confirming the URL responds. If the app needs a database URL, API origin, or mode flag, check that the variable exists in the runner's environment and that the server reports the intended startup state. Missing configuration may let a process listen while its health route remains unhealthy. That is a different failure from a command that never launched.

4. Choose a Readiness URL That Actually Signals Ready

An HTTP listener can open before the application can serve a useful page. Conversely, a valid app can look unavailable if webServer.url points to a route returning 404. Playwright's URL probe accepts the documented status classes, including 401 and 403, but a route that redirects to a failing page or returns 500 will not establish readiness. Probe the exact configured URL, including its path and trailing slash, from the test environment.

A minimal Node server can provide an unambiguous health route without a framework dependency. Save the following as server.mjs in a scratch Playwright project. It uses only built-in Node APIs:

import { createServer } from 'node:http';

const host = '127.0.0.1';
const port = Number(process.env.PORT ?? 4173);
createServer((request, response) => {
  if (request.url === '/health') {
    response.writeHead(204);
    response.end();
    return;
  }
  response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
  response.end('<h1>Ready</h1>');
}).listen(port, host, () => console.log(`Listening on http://${host}:${port}`));

Install the runner and launch this fixture in two terminals:

npm init -y
npm install -D @playwright/test
npx playwright install chromium
PORT=4173 node server.mjs
curl -i http://127.0.0.1:4173/health
curl -i http://127.0.0.1:4173/

The first response should be 204 No Content; the second should contain Ready. Now set command: 'node server.mjs', url: 'http://127.0.0.1:4173/health', and use.baseURL: 'http://127.0.0.1:4173' in your Playwright config. In a real app, use a route that becomes successful only after essential startup work is complete. A health check that always returns 200 while the app is unusable can shift the error into test navigation rather than solve it. If your product intentionally returns 401 at its health URL, Playwright may already accept it; confirm the actual final response before changing authentication.

5. Measure a Slow Startup Before Raising webServer.timeout

Cold CI machines may need time for dependency resolution, compilation, migrations, or service initialization. When the command remains alive, logs advance normally, and the correct URL becomes healthy after the current limit, a longer startup timeout is justified. First measure it. A timeout of two minutes is an example budget, not a universal recommendation.

Start the actual command in one shell and time a readiness probe in another. This portable Node command checks the same HTTP endpoint until it responds successfully or its own diagnostic limit expires:

node -e 'const u="http://127.0.0.1:4173/";const t=Date.now();const check=async()=>{try{const r=await fetch(u);if(r.ok){console.log(`ready in ${Date.now()-t}ms`);return}}catch{}if(Date.now()-t>180000){console.error("not ready after 180000ms");process.exitCode=1;return}setTimeout(check,1000)};check()'

Run npm run dev -- --host 127.0.0.1 --port 4173 --strictPort just before the probe. This illustrative checker treats only 2xx as healthy, so adjust it if your Playwright URL deliberately returns an accepted 3xx or 4xx status. If the measured cold start is around 75 seconds, give setup a reasonable margin and make the build path efficient:

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

export default defineConfig({
  webServer: {
    command: 'npm run dev -- --host 127.0.0.1 --port 4173 --strictPort',
    url: 'http://127.0.0.1:4173/',
    timeout: 120_000,
    stdout: 'pipe',
    stderr: 'pipe',
    reuseExistingServer: !process.env.CI,
  },
  use: { baseURL: 'http://127.0.0.1:4173' },
});

Verify by running npx playwright test from a clean startup, then compare its server logs with the measurement. A generous timeout is appropriate only when the app eventually becomes healthy. If startup time grows on every run, investigate compilation, network calls during boot, and resource pressure. Avoid compensating for a failing health endpoint with a larger number. The CI flakiness reduction guide discusses broader resource and scheduling issues.

6. Stop Reusing an Unrelated or Stale Server

reuseExistingServer: true is convenient on a developer laptop, but it can make a passing test use a process unrelated to the current checkout. With reuse disabled, Playwright reports a port conflict instead of silently adopting the listener. The timeout itself often appears after a stale service is stopped or after a different process takes over the port; the underlying issue is ownership of that endpoint.

Use this policy in the config:

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

export default defineConfig({
  webServer: {
    command: 'npm run dev -- --host 127.0.0.1 --port 4173 --strictPort',
    url: 'http://127.0.0.1:4173/',
    reuseExistingServer: !process.env.CI,
    stdout: 'pipe',
    stderr: 'pipe',
  },
  use: { baseURL: 'http://127.0.0.1:4173' },
});

Check the port before the run. On macOS or Linux, lsof shows the process listening on the intended address; curl shows its response:

lsof -nP -iTCP:4173 -sTCP:LISTEN
curl -i --max-time 5 http://127.0.0.1:4173/
CI=1 npx playwright test

The CI=1 command makes the configuration refuse reuse. If lsof lists a process, identify it before stopping anything; another project may legitimately own that port. The healthy fix is a dedicated test port or a controlled teardown, not killing arbitrary processes. Where several local branches run at once, assign separate ports to their app servers and Playwright configs. A response from the wrong application can satisfy a simple root URL probe, so a distinctive health body or endpoint is useful for manual verification even though Playwright's built-in readiness check examines status rather than response content.

7. Fix Docker Host and Container Network Mismatches

127.0.0.1 means the current network namespace. Inside a test container, it refers to that container, not the host machine or another Compose service. A server reachable from your laptop can therefore time out from Playwright inside Docker. In the container that hosts the app, bind the server to 0.0.0.0; in the test container, use the app service's DNS name and internal port. Do not use the host-published port when the containers share a Compose network.

For a Compose stack with an app service, this is a complete service sketch. It assumes the repository contains an npm dev script that accepts Vite's host, port, and strict port flags:

services:
  app:
    image: node:lts
    working_dir: /workspace
    volumes:
      - .:/workspace
    command: sh -c 'npm ci && npm run dev -- --host 0.0.0.0 --port 4173 --strictPort'
    ports:
      - '4173:4173'
    healthcheck:
      test: ['CMD', 'node', '-e', 'fetch("http://127.0.0.1:4173/").then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))']
      interval: 2s
      timeout: 3s
      retries: 60
  tests:
    image: mcr.microsoft.com/playwright:v<your-playwright-version>-noble
    working_dir: /workspace
    volumes:
      - .:/workspace
    command: sh -c 'npm ci && npx playwright test'
    depends_on:
      app:
        condition: service_healthy

The health check runs inside the app container and tests its internal listening address.

Replace the image placeholder with the version matching the installed Playwright package; do not copy a random published tag. Because Compose starts and checks the app, omit webServer from the container's Playwright config. Keep navigation pointed at the service name:

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

export default defineConfig({
  use: { baseURL: 'http://app:4173' },
});

Verify from the test container with docker compose run --rm tests; its smoke test should navigate to http://app:4173/. If you need a direct probe, run docker compose exec app node -e 'fetch("http://127.0.0.1:4173/").then(r => console.log(r.status))' after startup. For Docker networking and image details, see the Playwright Docker guide and Compose test environment guide. The official Docker documentation also explains matching the image to the Playwright package version.

8. Isolate a Multi-Service Startup Race

A frontend may be ready while its API, authentication stub, or database-backed service is still starting. If webServer is an array, name each entry and probe its own URL. Do not point both entries to the frontend's root route. A single shared probe can report ready while a dependency remains unavailable, or keep waiting without telling you which process owns the failure.

Here is the real Playwright array form for a frontend and a local API, assuming the repository defines dev:web and dev:api scripts that serve the shown ports:

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

export default defineConfig({
  use: { baseURL: 'http://127.0.0.1:4173' },
  webServer: [
    {
      name: 'API',
      command: 'npm run dev:api',
      url: 'http://127.0.0.1:3333/health',
      timeout: 120_000,
      stdout: 'pipe',
      stderr: 'pipe',
      reuseExistingServer: !process.env.CI,
    },
    {
      name: 'Frontend',
      command: 'npm run dev:web',
      url: 'http://127.0.0.1:4173/',
      timeout: 120_000,
      stdout: 'pipe',
      stderr: 'pipe',
      reuseExistingServer: !process.env.CI,
    },
  ],
});

Replace script names and ports with the actual package scripts; npm run will show what the project provides. Verify each service independently before the combined run:

curl -i --max-time 5 http://127.0.0.1:3333/health
curl -i --max-time 5 http://127.0.0.1:4173/
npx playwright test

The curl checks require both services to be running in separate terminals. If the API takes longer than the frontend, consider an API health endpoint that verifies its essential dependencies, and make the frontend tolerate the startup order your deployment actually uses. A named log line pinpoints which readiness condition timed out. The array starts services as part of setup, but do not rely on one command finishing before another begins; express real service dependencies in the app or orchestration layer.

How to Verify the Fix

Use a focused smoke test that proves the server served the page under the same Playwright configuration that failed. Put this in tests/server-ready.spec.ts after the app and config examples have been adapted to your project:

import { test, expect } from '@playwright/test';

test('configured server serves the home page', async ({ page }) => {
  const response = await page.goto('/');
  expect(response).not.toBeNull();
  expect(response?.ok()).toBe(true);
  await expect(page.locator('body')).toBeVisible();
});

For the minimal server.mjs fixture above, save a playwright.config.ts with command: 'node server.mjs', url: 'http://127.0.0.1:4173/health', and baseURL: 'http://127.0.0.1:4173'. Do not leave the manually started fixture running when reuseExistingServer is false. Then run:

npx playwright test tests/server-ready.spec.ts --project=chromium

The --project=chromium flag requires a project named chromium; if your config has no named projects, run npx playwright test tests/server-ready.spec.ts instead. A successful output includes one passing spec. A failure before the test title still points to server setup. A failure at page.goto('/') means the readiness URL may have succeeded while the page URL did not. A failed body assertion means the browser reached the server but did not render the expected page. Those are different next investigations.

Repeat the test once with CI=1 so local server reuse cannot mask the startup path. Inspect the [WebServer] logs or use DEBUG=pw:webserver npx playwright test tests/server-ready.spec.ts if the initial output is insufficient. The debug namespace is documented in the Playwright web server API. For a CI-specific failure, execute the same curl probe from the CI job or container, not from your workstation. Finally, run the full suite to verify that the repaired startup configuration works with all required projects.

Prevent It From Coming Back

Keep one declared origin for the app and reuse it in webServer.url and use.baseURL. Pin the host and port in the startup command, fail on port conflicts, and make the health URL cheap and intentional. Keep stdout: 'pipe' and stderr: 'pipe' during troubleshooting so a failed compile or missing variable is visible. Once the setup is stable, quieter output can be a deliberate choice, but preserve a way to turn diagnostics back on.

In CI, run npm ci from the correct package directory and install the browsers with npx playwright install --with-deps when using a Linux runner outside the Playwright image. The GitHub Actions for Playwright guide covers a full workflow. Keep the application boot command distinct from a build-only command, and measure cold startup after substantial dependency or framework changes. If the app needs migrations or external services, make those preconditions explicit in the pipeline rather than hoping the web server timeout absorbs them.

Use reuseExistingServer: !process.env.CI for local convenience and clean CI ownership. For Docker, use a service hostname from the test container and match the Playwright image tag to the package version. If a readiness route returns success before the app is operational, fix the route's semantics or add a meaningful smoke assertion. A short server setup check in CI catches a broken launch earlier than a broad browser suite and leaves clearer logs.

Interview Questions and Answers

Q: What does this error say about test execution?

Playwright did not confirm the configured server before its startup deadline. The spec body has not begun, so I inspect the launch command and readiness probe before changing assertion timeouts.

Q: Which timeout would you change after measuring a slow cold boot?

I would change webServer.timeout for the slow service, with a margin based on measured startup time. I would not use the top-level test timeout because it governs a different phase.

Q: How do webServer.url and use.baseURL differ?

The URL is a readiness target. baseURL is used to resolve relative navigation in tests. I keep their origins aligned but may use a /health path for readiness.

Q: Why can Docker pass a host curl check while Playwright times out?

The host and test container have different loopback interfaces. From the test container, I probe the app by its service hostname and internal port, while binding the app to an interface other containers can reach.

Q: When is server reuse risky?

Reuse can select an existing listener that belongs to another checkout or stale build. I allow it for local development and disable it in CI so the run proves its own app starts.

Q: What evidence distinguishes a bad URL from slow startup?

I run the server command independently, inspect the printed address, and poll the exact URL while timing the result. If it becomes healthy after the budget, startup is slow; if it never responds at that address, I investigate binding, route, and process health.

Common Mistakes

  • Increasing test.setTimeout() for a failure emitted by config.webServer. Change the startup configuration after diagnosing the process.
  • Treating any open port as an operational app. Prefer an HTTP URL whose response means essential startup completed.
  • Using localhost inside a container to reach another container. Use the Compose service name and internal port.
  • Hiding stderr while the server is crashing. Pipe it during investigation and read the first application error.
  • Running a build-only command as webServer.command. Start a long-lived server after the build.
  • Assuming depends_on proves application readiness. Probe the service from the test container.
  • Keeping reuseExistingServer: true in CI. That can adopt a stale listener and produce misleading green runs.
  • Copying an image tag from a tutorial without matching the installed Playwright package.
  • Testing a different URL with curl than the one configured in Playwright. Copy the exact URL, including path.

Conclusion

To fix Playwright Timed out waiting from config.webServer, first reproduce the startup command and probe the configured URL from the runner's own environment. Repair mismatched addresses, failed commands, unhealthy routes, or network boundaries; raise webServer.timeout only when measured startup genuinely exceeds the budget. Keep a focused smoke test and visible server logs so the next failure names the broken layer quickly.

Interview Questions and Answers

How would you triage a Playwright config.webServer timeout?

I first run the configured command from its configured working directory and capture stdout and stderr. Then I curl the exact readiness URL from the runner environment. The result separates launch failure, address mismatch, unhealthy route, and slow startup.

What is the difference between webServer.timeout and test timeout?

webServer.timeout bounds startup readiness during runner setup. Test timeout bounds execution of a test and its associated hooks or fixtures. Changing the test timeout cannot make a server readiness probe wait longer.

Why use a URL instead of the deprecated port readiness property?

A port can accept connections before the application serves meaningful HTTP responses. A URL checks an actual endpoint and can identify a bad route or unhealthy response. I choose a lightweight route tied to essential startup.

How can reuseExistingServer hide a defect?

The runner may attach to a listener left by another checkout or run. Tests then pass against the wrong build without proving the configured command works. I disable reuse in CI and inspect port ownership locally.

How do you diagnose a container-only web server timeout?

I run the readiness probe inside the same container as Playwright. I check that the app binds to 0.0.0.0 and that the test uses the Compose service name and internal port. A successful host curl alone does not prove container reachability.

When is increasing webServer.timeout the correct fix?

It is justified when logs show a healthy startup that consistently completes after the current limit, especially on cold CI machines. I measure the time to the exact readiness URL, set a modest margin, and investigate any continued growth.

How would you handle multiple configured web servers?

I give each entry a name, command, and service-specific readiness URL, then probe each independently. This makes the failing dependency visible in logs. I avoid assuming array order creates a reliable dependency sequence.

Frequently Asked Questions

What does Timed out waiting from config.webServer mean?

Playwright did not observe the configured server readiness condition within webServer.timeout. The failure happens during runner setup, before the test body executes. Check the server process and exact readiness URL.

How do I increase the Playwright webServer startup timeout?

Set timeout in the webServer object, for example timeout: 120_000. Measure how long the correct URL takes to become healthy first. A larger limit does not repair a wrong path, failed command, or inaccessible container host.

Why does webServer work locally but time out in CI?

A local process may already occupy the port, or CI may have a colder boot, missing environment value, different working directory, or unavailable service. Set reuseExistingServer to !process.env.CI, pipe server output, and probe the URL in the CI job.

Does changing the Playwright test timeout fix webServer startup?

No. The top-level test timeout applies to test execution, whereas webServer.timeout applies while Playwright waits for the application server. Change the setting for the phase shown by the error.

Which HTTP status codes count as ready for webServer.url?

Playwright documents 2xx, 3xx, 400, 401, 402, and 403 as acceptable readiness responses, with redirects followed. A 404 or 500 from the final destination does not establish readiness. Probe the exact URL to see its real status.

Why does localhost fail from a Playwright Docker container?

Loopback inside the container points back to that container. If the app runs in another Compose service, use its service hostname and internal port, and bind the app to 0.0.0.0.

Should I use reuseExistingServer in CI?

Usually disable it in CI so the run must start or own the intended app and expose port conflicts. Local reuse can be useful while developing, but verify the listener belongs to the current checkout.

Related Guides