Resource library

QA How-To

How to Fix Playwright net ERR_CONNECTION_REFUSED

Fix Playwright net ERR_CONNECTION_REFUSED by validating the target URL, server readiness, baseURL, ports, containers, CI networking, and test isolation.

25 min read | 3,244 words

TL;DR

To fix Playwright net ERR_CONNECTION_REFUSED, confirm the final navigation URL and make sure a server is listening on that host and port from the browser's environment. Configure Playwright `webServer` with the same readiness URL and `baseURL`, bind containerized apps to a reachable interface, and inspect server logs before adding retries or longer timeouts.

Key Takeaways

  • ERR_CONNECTION_REFUSED means the browser reached the host but no service accepted the TCP connection on the requested address and port.
  • Verify the exact URL from the same runtime namespace as Playwright before changing navigation timeouts or test retries.
  • Use Playwright's webServer configuration to start the application and wait for a readiness URL in local and CI runs.
  • Keep baseURL, application bind address, protocol, and exposed port aligned across host, container, and pipeline environments.
  • Remember that localhost inside a container refers to that container, not automatically to the host or another service.
  • Test a readiness endpoint that proves required dependencies are available, not only that a process has opened a socket.
  • Treat intermittent refusals as lifecycle evidence, such as early navigation, server restarts, worker cleanup, or exhausted resources.

To fix playwright net ERR_CONNECTION_REFUSED, make the target application reachable at the exact scheme, host, and port used by the Playwright browser. The error normally means no process accepted the connection, so increasing a locator timeout or changing waitUntil cannot repair it.

The fastest path is to print the resolved URL, probe it from the same host or container that runs Playwright, and inspect the application process. Then make startup and readiness part of the test configuration. This guide covers local development, baseURL, Playwright webServer, Docker, CI services, IPv4 and IPv6, HTTPS mistakes, transient restarts, and evidence-driven triage.

TL;DR

Failure pattern First check Likely repair
Refused immediately on every run Is anything listening on the requested port? Start app or correct URL and port
Fails only at suite startup Does navigation begin before readiness? Configure webServer.url and readiness
Works with full URL, fails with relative path Is baseURL correct and loaded? Align config and environment variable
Works on host, fails in test container What does localhost mean inside that container? Use service DNS or a reachable host address
Port is open but app still not ready Does health route validate dependencies? Use a meaningful readiness endpoint
Fails after several tests Did server crash or restart? Inspect logs, memory, lifecycle, and cleanup
HTTPS URL targets HTTP server Are scheme and certificate setup correct? Use correct protocol or configure HTTPS properly
IPv6 localhost behaves differently Which address is bound and resolved? Align 127.0.0.1, ::1, or bind interface

Start with a transport probe:

curl --fail --show-error --verbose http://127.0.0.1:4173/health

Run it in the same CI job, container, or machine namespace that launches Playwright. A successful curl from your laptop does not prove the test container can connect.

1. What ERR_CONNECTION_REFUSED Actually Means

net::ERR_CONNECTION_REFUSED is a browser network error, commonly surfaced by page.goto(). At the transport level, the connection attempt was actively rejected or no listener existed at the selected address and port. The browser did not receive an HTTP status such as 404, 500, or 503 because an HTTP exchange never completed.

This distinction prevents wasted debugging. A 404 means the server responded but the route is wrong. A 503 means a responding gateway or application reports temporary unavailability. ERR_NAME_NOT_RESOLVED points to DNS. ERR_CERT_AUTHORITY_INVALID points to TLS trust. ERR_CONNECTION_TIMED_OUT suggests dropped packets, routing, firewall, or an unresponsive endpoint. Connection refused focuses attention on listener, address, port, scheme, and lifecycle.

Playwright often reports a line such as:

page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:3000/login

Copy that exact URL. Do not rely on what you think baseURL contains. Environment substitution, config selection, redirects, or a missing slash may produce a different destination. Confirm whether the refusal occurs on the initial URL or after a redirect to another host or port.

A browser retry can occasionally outlast a startup race, but that does not establish correct orchestration. The server should be ready before the test begins.

2. Connection Error Reference Table

Use the browser's wording and the server evidence together.

Browser or HTTP result Layer reached Typical investigation
ERR_CONNECTION_REFUSED Host route, but no accepting listener Process, bind address, port, container namespace
ERR_CONNECTION_TIMED_OUT No timely transport response Firewall, routing, proxy, unreachable network
ERR_NAME_NOT_RESOLVED DNS lookup failed Hostname, DNS configuration, service name
ERR_CERT_AUTHORITY_INVALID TLS handshake rejected trust Certificate chain, hostname, test CA policy
HTTP 404 Web server responded Route, base path, deployment artifact
HTTP 401 or 403 Application or gateway responded Authentication, authorization, headers
HTTP 500 Application responded with failure Server exception, data, dependency logs
HTTP 502 or 503 Proxy or service responded Upstream readiness, gateway routing, capacity

Playwright's navigation timeout is only a time budget. It does not transform a refused TCP connection into a listener. Likewise, ignoreHTTPSErrors concerns certificate errors after a TLS server accepts the connection. It cannot start a missing HTTPS service.

Capture application stdout and stderr beside the Playwright report. A bind error such as address already in use, a missing environment variable, a failed database migration, or an immediate process exit often explains the browser message directly.

3. Fastest Way to Fix Playwright net ERR_CONNECTION_REFUSED

Verify the destination in four layers: configuration, process, socket, and HTTP readiness.

  1. Print the effective baseURL or full URL without exposing secrets.
  2. Confirm the application process is still running.
  3. Check that it listens on the expected interface and port.
  4. Request a health route from the Playwright runtime.
  5. Navigate only after the route succeeds.

A minimal Playwright test can make the dependency explicit:

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

test('application is reachable before UI flow', async ({ page, request }) => {
  const health = await request.get('/health');
  expect(health.status()).toBe(200);

  await page.goto('/login');
  await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
});

This example assumes baseURL is configured. The API check is diagnostic and can be a useful smoke assertion, but do not add it to every test. Startup orchestration should normally guarantee readiness once, while feature tests focus on behavior.

If the application runs separately, start it using the repository command and probe the exact port. If Playwright should own startup, configure webServer as shown in the next section. If the server exits, fix its log error before rerunning the browser.

The Playwright timeout guide helps when a reachable page loads but a later operation times out. A refused connection is earlier in the stack.

4. Configure Playwright webServer Correctly

Playwright Test can start a local application before the suite and wait for a URL to become available. This removes shell background-process races and makes local and CI startup consistent.

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

const port = 4173;
const baseURL = `http://127.0.0.1:` + port;

export default defineConfig({
  use: {
    baseURL,
    trace: 'retain-on-failure',
  },
  webServer: {
    command: 'npm run preview -- --host 127.0.0.1 --port 4173',
    url: baseURL + '/health',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
    stdout: 'pipe',
    stderr: 'pipe',
  },
});

The command must keep the server process in the foreground. Do not append shell background operators. Playwright manages the process lifecycle and waits for url. The route should return a successful status when the application is ready for tests.

reuseExistingServer is convenient locally, but CI should normally start a known instance. Setting it from !process.env.CI avoids accidentally testing an unrelated process on a shared runner. If the port is occupied locally, verify the existing server is the right build and configuration.

The webServer timeout is separate from test and navigation timeouts. Give builds and server startup a realistic budget, then diagnose if they exceed it. A very large timeout should not hide a process that exited immediately.

For multiple required processes, Playwright supports an array of web server configurations. Give each service a distinct command and readiness URL. Keep dependencies explicit instead of sleeping for an estimated combined startup time.

5. Align baseURL, Relative Navigation, and Environment Variables

When baseURL is configured, Playwright resolves relative URLs used by page.goto(), page.waitForURL(), and request-context methods. A wrong environment value can send every test to an unused port.

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

const baseURL = process.env.E2E_BASE_URL ?? 'http://127.0.0.1:4173';

export default defineConfig({
  use: { baseURL },
});

Validate external input early:

const parsed = new URL(baseURL);
if (!['http:', 'https:'].includes(parsed.protocol)) {
  throw new Error('E2E_BASE_URL must use http or https');
}
console.log('Playwright target:', parsed.origin);

Log the origin, not credentials, tokens, or sensitive query parameters. Use the same normalized value for use.baseURL and webServer.url when Playwright starts the app. A common error is starting port 4173 while baseURL still points to 3000.

Pay attention to path prefixes. If the deployed application lives under /qa/, URL resolution depends on whether the base contains a trailing slash and whether navigation starts with /. Use the JavaScript URL class in a small test if the result is unclear.

Keep environment-specific base URLs in CI variables or project configuration, not scattered across tests. Full external URLs are appropriate when a test intentionally leaves the application, but ordinary feature navigation should use one controlled origin.

6. Diagnose Ports, Bind Addresses, and IPv4 Versus IPv6

A server can run while listening somewhere the test cannot reach. Check both the port and bound interface. Binding only to 127.0.0.1 accepts IPv4 loopback connections on that machine. Binding only to ::1 accepts IPv6 loopback. Binding to 0.0.0.0 makes an IPv4 service available on all container or host interfaces, subject to firewall and port publishing.

On macOS or Linux, inspect listeners with available system tools:

lsof -nP -iTCP:4173 -sTCP:LISTEN
curl --fail http://127.0.0.1:4173/health
curl --fail http://localhost:4173/health

If one curl works and the other fails, hostname resolution and bind family may differ. Choose a consistent address in server command, readiness URL, and baseURL. Do not edit the system hosts file as a first response.

A wrong protocol can look similar. An HTTP server on port 4173 does not automatically accept https://127.0.0.1:4173. Confirm whether TLS termination happens in the application, a reverse proxy, or nowhere in the test environment.

Port conflicts can also cause refusal indirectly. The intended server may fail to bind because another process owns the port, then exit. Playwright later visits the port after that other process stops or never provided the expected route. Treat address-in-use messages in server logs as primary evidence.

7. Fix Localhost in Docker and Container Networks

Inside a container, localhost refers to that container's own network namespace. If the application runs in another container, use the service name on the shared network. If it runs on the host, use the platform's supported host gateway name or network configuration.

A Docker Compose pattern keeps both services on the default project network:

services:
  app:
    build: .
    command: npm run preview -- --host 0.0.0.0 --port 4173
    expose:
      - "4173"
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4173/health').then(r => { if (!r.ok) process.exit(1) })"]
      interval: 2s
      timeout: 2s
      retries: 30

  e2e:
    build:
      context: .
      dockerfile: Dockerfile.e2e
    environment:
      E2E_BASE_URL: http://app:4173
    depends_on:
      app:
        condition: service_healthy

The application binds to 0.0.0.0 so other containers can reach it, while Playwright uses DNS name app. Publishing 4173:4173 is needed for host access, but containers on the same network can use the exposed service port directly.

Do not use a Compose service name from a Playwright process running directly on the host unless that name is resolvable there. Network names are scoped. State where the browser process lives, where the web server lives, and which address joins those namespaces.

The Docker for Playwright guide covers browser images, users, shared memory, and version alignment beyond application networking.

8. Build a Readiness Check That Means Ready

A port can open before the application is usable. A framework may start listening, then load routes, connect to a database, run migrations, or warm required data. If webServer.url probes a static root too early, UI tests can race later initialization even though connection refusal disappears.

Design a dedicated readiness route with clear semantics. It should return success only when the dependencies required by the tested flows are available. Keep it fast, deterministic, and free of sensitive configuration details. Separate liveness, which says the process should be restarted or not, from readiness, which says it can receive test traffic.

Do not make readiness perform destructive setup on every poll. Playwright or an orchestrator may request it repeatedly. Seed test data through an explicit setup API, fixture, or database step with ownership and cleanup.

A simple external probe in Node 20 or later is:

node -e "fetch('http://127.0.0.1:4173/health').then(async r => { console.log(r.status, await r.text()); if (!r.ok) process.exit(1) }).catch(e => { console.error(e); process.exit(1) })"

If the route is behind authentication, consider a separate non-sensitive readiness route accessible within the test network. Do not place credentials directly in logged URLs. For production-like staging environments, coordinate health contracts with platform owners rather than weakening gateway security.

9. Fix CI Startup Ordering and Process Lifecycle

CI scripts often start the server with &, immediately run tests, and hope the build is ready. That creates a nondeterministic race. Prefer Playwright webServer for a local app or the CI platform's managed service and health checks for separate containers.

If the pipeline starts the server separately, keep the process alive, retain its logs, and use an explicit readiness loop supplied by the platform or a supported tool. Do not swallow startup failures. Ensure the test step runs in the same job or network context when that is required.

A shell probe can be bounded without arbitrary sleep as the only condition:

for attempt in 1 2 3 4 5 6 7 8 9 10; do
  if curl --fail --silent http://127.0.0.1:4173/health > /dev/null; then
    exit 0
  fi
  sleep 1
done
exit 1

This loop is suitable when configuration cannot use webServer, but preserve application logs on failure. The readiness endpoint, not elapsed time, controls success.

Check cleanup. A global teardown or job trap can terminate the server before all shards finish. A child process may also receive a hangup when the shell step ends. Managed containers, service steps, or Playwright-owned processes provide clearer lifetime boundaries.

For broader pipeline patterns, see GitHub Actions for Playwright and Jenkins pipeline for Playwright.

10. Handle Redirects, Proxies, HTTPS, and Authentication Gateways

The initial URL may accept connections and then redirect to a host that refuses them. Capture request and navigation information before assuming the configured origin failed:

page.on('request', (request) => {
  if (request.isNavigationRequest()) {
    console.log('navigation request:', request.method(), request.url());
  }
});

page.on('requestfailed', (request) => {
  console.error('request failed:', request.url(), request.failure()?.errorText);
});

await page.goto('/login');

Use this logging temporarily or attach it conditionally on failure, since broad network logs can expose sensitive data and create noise. Redact tokens and credentials.

A reverse proxy might listen on port 443 while the application listens internally on 4173. Decide whether Playwright should test through the proxy or directly. Then configure one coherent URL. Testing through the proxy validates routing, TLS, headers, and authentication integration, while direct access isolates the application. Both can be valuable, but they answer different questions.

ignoreHTTPSErrors: true can allow a controlled test environment with an untrusted certificate, but it does not fix a refusal and should not be a general production-testing policy. Prefer installing the approved test certificate authority when practical.

Corporate proxy variables can also redirect traffic unexpectedly. Most automation should bypass proxies for controlled loopback or internal service names through approved NO_PROXY configuration. Coordinate with network policy rather than disabling protections globally.

11. Diagnose Intermittent Connection Refusals

A refusal that occurs after tests have already passed points to lifecycle or resource instability. The application may restart, crash, run out of memory, lose a dependency, or be killed by test cleanup. A parallel worker can also reset shared infrastructure while another worker navigates.

Correlate timestamps across Playwright and server logs. Capture process exit code, restart count, memory limit events, container health, and the test that ran immediately before the refusal. If a test intentionally restarts the service, isolate it from ordinary parallel tests or give it dedicated infrastructure.

Do not add navigation retries until you know whether retrying is semantically safe. A retry after refusal may be harmless for a GET navigation, but it can hide server instability and may repeat prior state-changing setup. Playwright test retries are useful for measuring and containing flakiness, not as the root fix.

Sharding increases load and can expose listener backlog, dependency pool, or resource problems. That does not mean sharding is wrong. Size the environment, limit workers when appropriate, and use metrics to find the constrained component. A server that crashes under expected test concurrency is valuable evidence.

Also inspect test teardown. One worker should not own a shared server process if other workers depend on it. Keep suite infrastructure above worker scope or allocate a server per worker with a distinct port.

12. Create a Minimal Network Diagnostic Test

A small diagnostic separates browser navigation from application functionality. It should report the configured origin, make an API request, and then load a minimal route.

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

test('network dependency diagnostic', async ({ page, request, baseURL }) => {
  if (!baseURL) {
    throw new Error('Playwright baseURL is required');
  }

  console.log('target origin:', new URL(baseURL).origin);

  const response = await request.get('/health', { timeout: 10_000 });
  expect(response.status()).toBe(200);

  const result = await page.goto('/health', {
    waitUntil: 'domcontentloaded',
    timeout: 10_000,
  });

  expect(result).not.toBeNull();
  expect(result?.status()).toBe(200);
});

Run it in the failing project and network environment. If the API request and page navigation both fail, investigate reachability. If the API request succeeds but browser navigation fails, inspect proxy, browser context, redirect, service worker, and URL differences. If health succeeds but the feature page fails with an HTTP response, the problem is above the transport layer.

Keep this test as a targeted smoke check only if it adds value to the pipeline. Otherwise, use it during diagnosis and rely on webServer readiness plus feature coverage.

13. Prevent and Fix Playwright net ERR_CONNECTION_REFUSED Regressions

Make reachability an explicit contract in repository and pipeline code. Define one source of truth for the application origin, start required services deterministically, wait on meaningful readiness, retain logs, and shut infrastructure down only after all dependent workers finish.

A practical prevention checklist is:

  • Pin the application port or allocate it programmatically without collisions.
  • Use the same URL for webServer readiness and baseURL origin.
  • Bind container services to an interface reachable from their consumers.
  • Use service DNS names between containers, not accidental localhost.
  • Fail fast when required environment variables are malformed.
  • Attach server logs and container status to failed CI jobs.
  • Keep test data setup separate from health polling.
  • Monitor process exits and restarts during parallel runs.

Avoid global hard-coded URLs in page objects. Navigation methods should use relative application paths unless a cross-origin flow is intentional. Document which component owns server startup for local, preview, container, and staging modes.

After a repair, prove it from a clean environment. Stop any manually started local server, clear the accidental process dependency, and run the documented command. In CI, rerun without relying on a previous job's container or workspace. Reproducibility is the final test of the fix.

Interview Questions and Answers

Q: What does net::ERR_CONNECTION_REFUSED mean in Playwright?

It means the browser could not establish a connection because no service accepted it at the requested address and port, or the network actively rejected it. I investigate server process, listener, URL, bind interface, and lifecycle before changing test timeouts.

Q: How is connection refused different from an HTTP 500?

An HTTP 500 proves a server accepted the connection and returned an application error. Connection refused occurs before an HTTP response exists, so application route assertions have not started.

Q: How does Playwright webServer prevent this failure?

It starts the configured command, waits for a readiness URL, keeps the process associated with the suite, and shuts it down afterward when appropriate. This replaces an uncontrolled background-process race with explicit orchestration.

Q: Why does localhost fail in Docker?

Each container has its own loopback interface. A Playwright container using localhost looks inside itself, so an app in another container must be reached through its service DNS name and port on the shared network.

Q: What should a readiness endpoint verify?

It should verify that the application can serve the tested workflows and that required dependencies are ready. It should be fast, safe to poll, non-destructive, and avoid exposing sensitive details.

Q: Would you retry page.goto() after connection refused?

Not as the first fix. I make startup and service lifecycle deterministic, then consider a bounded retry only for a documented transient network boundary where repeating navigation is safe and observable.

Q: How do you debug an intermittent refusal after several tests?

I correlate browser failure time with server exits, restarts, memory events, container health, and teardown logs. I also check whether parallel workers share infrastructure with incorrect ownership.

Q: Why does increasing the navigation timeout not solve a refusal?

A timeout only changes how long Playwright waits. It does not create a listener or correct a wrong host, port, protocol, or container route.

Common Mistakes

  • Increasing locator or navigation timeouts when no server accepts connections.
  • Running tests immediately after a background server command with no readiness check.
  • Starting one port while baseURL points to another.
  • Using localhost between separate containers.
  • Binding an app to loopback when another container must reach it.
  • Probing the server from the laptop instead of the Playwright runtime.
  • Treating a liveness route as proof that databases and required services are ready.
  • Ignoring redirects to a refused secondary origin.
  • Using ignoreHTTPSErrors for a non-TLS connection problem.
  • Allowing teardown in one worker to stop infrastructure used by others.
  • Retrying tests until a crashing server happens to stay alive.
  • Discarding application stdout, stderr, exit codes, and container health evidence.

Conclusion

To fix playwright net ERR_CONNECTION_REFUSED, work from the network boundary inward. Confirm the effective URL, prove a process listens on the correct interface and port, and request a meaningful readiness route from the same environment that runs the browser.

Then encode that dependency in webServer, container health, or CI service orchestration. Align baseURL with the reachable address, retain server logs, and investigate intermittent process loss instead of masking it with retries. Once a clean run can start the application, wait for readiness, and navigate without manual intervention, the repair is reproducible.

Interview Questions and Answers

What layers do you check for ERR_CONNECTION_REFUSED?

I check the effective URL, DNS or address resolution, process state, bound interface, listening port, container namespace, and readiness. I then inspect lifecycle logs to see whether the process exited or restarted. Each probe runs from the same network namespace as Playwright.

How would you configure a local app for Playwright Test?

I use `webServer` with a foreground start command, a meaningful readiness URL, bounded startup timeout, and piped logs. I set `use.baseURL` to the same origin and disable reuse in CI. This gives startup and teardown a clear owner.

What is wrong with starting a server using an ampersand before tests?

It does not prove readiness and can lose the process when the shell step ends. Failures become races, logs are often detached, and cleanup ownership is unclear. I prefer runner-managed orchestration and a health URL.

How do container service names affect Playwright navigation?

Service names resolve inside their shared container network, while localhost resolves within the current container. The base URL must use an address meaningful from the browser process's namespace. The application must also bind to a reachable interface.

How do you distinguish startup race from application crash?

A startup race occurs before initial readiness and is fixed by orchestration. A crash happens after readiness or during the suite, so I correlate process exit, health changes, resource events, and the preceding test activity. Timestamps distinguish the two timelines.

What evidence should CI retain?

CI should retain application stdout and stderr, process exit codes, container health and restart status, the Playwright trace and report, and the effective non-sensitive target origin. Timestamps should allow cross-correlation. Sensitive URLs and headers must be redacted.

When is an HTTP request probe useful?

It isolates reachability and readiness from page rendering. If both API request and browser navigation fail, the problem is likely transport or shared configuration. If only navigation fails, I inspect browser-specific routing, redirects, and context.

Why should test workers not own a shared server?

A worker-scoped teardown can stop the server while other workers still depend on it. Shared infrastructure needs suite-level ownership, or each worker needs an isolated instance and unique port. Cleanup must follow the same ownership boundary.

Frequently Asked Questions

How do I fix Playwright net ERR_CONNECTION_REFUSED?

Verify the exact target URL and start a server that listens on its host and port. Use Playwright `webServer` or CI service readiness so navigation begins only after the application is reachable.

Why does Playwright say ERR_CONNECTION_REFUSED on localhost?

The local server may be stopped, listening on another port or address family, or running in a different container namespace. Probe the URL from the same environment as Playwright and inspect the listener.

Can a longer page.goto timeout fix connection refused?

No. A longer timeout cannot create a missing server listener or correct the destination. Fix startup, address, port, protocol, or network routing first.

How should I start a Vite app before Playwright tests?

Configure `webServer.command` with the repository's start or preview command, set `webServer.url` to a readiness route, and use the same origin as `use.baseURL`. Bind to the interface required by the test environment.

Why does localhost work on my host but not inside Docker?

Container localhost refers to the container itself. Reach another Compose service by its service name, or use the platform-supported host gateway when the application intentionally runs on the host.

What is the difference between ERR_CONNECTION_REFUSED and a 404?

A refusal means no HTTP connection was established. A 404 means a server accepted the connection and responded that the requested route was not found.

Should a health endpoint check the database?

A readiness endpoint should check dependencies required by the tested flow, which may include the database. Keep the check fast, read-only, safe to poll, and separate from simple process liveness.

Related Guides