Skip to content
Back to Knowledge Base

Playwright web server readiness

When a Playwright config declares a webServer whose command runs an Nx task and that sets reuseExistingServer to true, @nx/playwright runs that task as a dependency of the Playwright tasks and infers a <targetName>--wait-for-webserver task that waits for the server to be ready. The Playwright plugin page covers the default workflow.

The command must be nx run <project>:<target> or nx <target> <project>, optionally prefixed by npx, yarn, bun, pnpm, pnpm exec, or pnpx. Nx doesn't recognize a command with extra arguments, such as npx nx run my-app:serve --port=4200, and infers neither the dependency nor the wait task for it.

A trailing :<configuration> in the nx run form is accepted, but a task dependency can't carry a configuration, so the dependency runs the target without it. Nx skips the wait task for such a command, since the server may not listen at the configured address.

Nx infers the wait task from the port a webServer sets, or from its url when port is unset, which is when Playwright checks the URL rather than the port. When the webServer sets neither, Playwright has nothing to check and starts its own server, and Nx infers no wait task.

Nx infers neither the dependency nor the wait task for a webServer that sets wait.stdout or wait.stderr. Playwright treats matching output from the server process it starts as the readiness signal and can capture values from it into the environment, so Nx leaves starting that server to Playwright. The same applies to a webServer that sets env. Only Playwright passes that environment to the command it starts, so a task-started server would run without it.

Nx also skips the wait task, keeping the dependency, when an env file the Playwright task loads changes how the server would be probed. The same goes for the Playwright config itself, such as one that calls dotenv.config(), since it runs before Playwright probes. The variables that count are the proxy variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY) for a webServer with a url, and NODE_EXTRA_CA_CERTS unless every such webServer sets ignoreHTTPSErrors. The proxy variables count in either spelling, and the lowercase one (http_proxy) wins over the uppercase one when both are set. That option doesn't cover the certificate of an https proxy that HTTPS_PROXY or ALL_PROXY routes through, so NODE_EXTRA_CA_CERTS counts whenever that's the route. That connection is also the only one either probe leaves to NODE_TLS_REJECT_UNAUTHORIZED, so it counts when that's the route and one side sets it to 0. With @playwright/test below 1.59.0, the probe also reads npm's npm_config_http_proxy, npm_config_https_proxy, npm_config_proxy, and npm_config_no_proxy, which npm run exports from an .npmrc proxy or no-proxy setting, so they count on those versions as well. With @playwright/test below 1.59.0, a probe tunnelled through an http proxy never verifies the certificate, so NODE_EXTRA_CA_CERTS doesn't count while an http proxy handles the https route, no https proxy handles the http route, and no NO_PROXY entry can send a redirect target direct. Node reads NODE_EXTRA_CA_CERTS when the process starts, so only its value in an env file counts, not one the config sets. Both probes follow redirects, so a variable that plays no part at the configured URL, such as HTTPS_PROXY for an http URL, still counts. Nx compares the routes those variables produce rather than their raw values. A variable that another one masks doesn't count, such as NO_PROXY when no proxy is configured or ALL_PROXY next to an explicit HTTP_PROXY and HTTPS_PROXY, and neither does an HTTP_PROXY written with or without its http:// scheme or a NO_PROXY list in a different order. A scheme-less proxy value takes the protocol it's used for. A scheme-less HTTPS_PROXY is an https proxy and a scheme-less ALL_PROXY follows each request's protocol, so either counts against the same value written with http://. The wait task runs with its own environment (its own env files included) rather than the Playwright task's and never loads the config, so it can't reproduce that probe and the tests fall back to Playwright's own reuseExistingServer probe.

The webServerTimeout plugin option sets how long, in milliseconds, the wait task waits before failing. It defaults to each web server's configured timeout, or 60000 when neither is set.

Set the waitForWebServer plugin option to false to opt out of the wait task. Nx still runs the web server as a task dependency, and the tests fall back to Playwright's own reuseExistingServer probe. Nx also still re-evaluates the config under the task's env, because the webServer.command the dependency is inferred from can itself read that env.

The readiness address comes from evaluating the Playwright config while Nx computes the project graph. Nx caches the result and rebuilds it when an env file the Playwright task would load, such as .env or .env.e2e, changes. An env file listed in .nxignore is an exception: Nx doesn't see it change, so the cached result stays stale. So is a workspace-root env file whose name comes from a target name containing /, such as .env.e2e/smoke, which Nx doesn't watch. The same file under a project root is watched.

A variable set only in the shell is part of that cache too, so changing it re-infers the address. The exception is variables Nx filters out of the project graph environment, such as terminal session details or CI platform internals. Changing one of those doesn't re-infer the address. CI itself isn't filtered, so changing it re-infers the address. Running nx reset also discards the cached address.

Files the config imports from outside its project, such as a shared helper in tools/, are not part of the cache either. Editing one doesn't refresh an already-cached address. Run nx reset to re-infer it.

A task dependency can't carry a target configuration, so the address is always inferred without one and a configuration-scoped env file such as .env.e2e.production never affects it. When such a file moves the server to another address, running the task with that configuration leaves the wait task probing the unconfigured address until it times out and fails the run. Set the waitForWebServer plugin option to false for such a setup.

The Playwright task runs from the project root, but Nx doesn't guarantee that the config is evaluated from there while computing the project graph. An address that depends on the working directory, such as a port read from a file at a relative path, can differ between the two, and the wait task then waits for the wrong address. Derive such an address from the config file's own directory instead: import.meta.dirname in an ESM config, such as the generated playwright.config.mts, or __dirname in a CommonJS one. Alternatively, set the waitForWebServer plugin option to false.

Nx re-evaluates the config under the task's env only when the graph-time evaluation found a webServer, so a webServer that only exists under the task's env is not detected. That re-evaluation runs the config in a child process started under the task's env, so an env file that sets Node startup options such as NODE_OPTIONS has them applied while Nx computes the project graph, not only when the task runs. If that re-evaluation fails or times out, Nx logs a warning, infers the task dependencies from the graph-time evaluation, and skips the wait task. The failed result is not cached, so the next project graph computation retries the evaluation.

Last updated: