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, which wait for its readyWhen check. With the waitForWebServer plugin option set to true, Nx also infers a <targetName>--wait-for-webserver task that probes the server's port or url before the tests run. The atomized CI tasks share that task when it would wait for the same servers, depend on the same serve tasks, and load the same env files. Otherwise they get their own <ciTargetName>--wait-for-webserver task. 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, bunx, bun x, 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) takes precedence 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.

Nx infers the wait task only when the waitForWebServer plugin option is true. Without it, the Playwright tasks wait for the serve task's readyWhen check. A serve task without one counts as ready once it starts, so Playwright's single reuseExistingServer probe can miss and start a second server next to the one Nx started. Set readyWhen on that target, or turn the wait task on for it. The wait task, the webServerTimeout and waitForWebServer plugin options, and the @nx/playwright:wait-for-webserver executor are deprecated and will be removed in Nx 25.

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.

Nx re-evaluates the config under the task's env whether or not it infers the wait task, 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.

When the wait task waits for a single server configured with a url, and the serve task reported its address in NX_READY_URL, the wait task takes the protocol and port from the reported address and keeps the path and query of the configured url. A localhost host stays as it is when the server listens on a loopback address. Any other host moves to the one the server reported.

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. Leave the waitForWebServer plugin option off 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, leave the waitForWebServer plugin option off.

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: