Home › Testing › cy.visit() Failed: Your Dev Server Is Not Running
Beginner 6 min · September 23, 2026

cy.visit() Failed: Your Dev Server Is Not Running

Start your dev server before Cypress runs and match baseUrl to it.

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 8 min
  • ✓Node.js installed with your project's dev server runnable via npm
  • ✓A Cypress project with cypress.config.js you can edit
  • ✓Basic comfort reading CI job logs and running curl
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • cy.visit() fails with ECONNREFUSED when no server listens on the URL's host and port — start the dev server first, then run Cypress
  • Set baseUrl in cypress.config.js so cy.visit('/') resolves correctly, and override it per environment with CYPRESS_BASE_URL
  • Never trust process-spawn as readiness — block Cypress with start-server-and-test or wait-on until the URL responds
  • In CI, bind the host explicitly and add a curl health check so a dead server fails fast with a clear message
✦ Definition~90s read
What is Cypress cy.visit Failed?

The cy.visit() command tells Cypress's browser to load a page, and the baseUrl option in cypress.config.js decides which origin relative visits resolve against. When you call cy.visit('/login') with baseUrl: 'http://localhost:3000', the browser requests http://localhost:3000/login — which requires your dev server to be running and listening on port 3000 at that exact moment.

★
Think of Cypress as a food critic and your dev server as the restaurant.

If nothing listens there, the OS refuses the connection and Cypress reports the visit as failed. This is a network-layer outcome, not a test-logic outcome, and no amount of retrying the assertion will change it.

CYPRESS_BASE_URL is the environment-variable override for baseUrl: setting it changes the target origin for the whole run without editing config, which is how one suite tests local, staging, and preview deployments. The catch is that only relative visits honor it — absolute URLs in specs bypass it silently. Many teams discover this when their staging job keeps testing localhost.

Readiness is the third piece. Modern dev servers compile on boot, so the process exists long before the port answers. Tools like start-server-and-test and wait-on close that gap by polling the URL until it responds, then launching Cypress. Without such a gate, every CI run is a race between the compiler and the test runner — and races produce exactly the intermittent visit failures teams mislabel as flaky tests.

Plain-English First

Think of Cypress as a food critic and your dev server as the restaurant. cy.visit() is the critic walking to the restaurant's address. If the restaurant is closed (server not running), at a different street than the critic wrote down (wrong baseUrl), or still unlocking the doors (not ready yet), the critic cannot review anything. The fix is not a better critic — it is confirming the restaurant is open before sending the critic over.

It is 9:40 AM, your pull request is green everywhere except the one Cypress job that matters, and the error is brutally unhelpful: cy.visit() failed trying to load some URL, connection refused. You did not change the test. You did not change the app. Rerun the job and it passes — or fails on a different spec this time. Congratulations, you have met the most common Cypress failure in existence, and it has almost nothing to do with Cypress.

In nearly every case, cy.visit() fails for one of three boring reasons: nothing is listening at that address, Cypress is aimed at the wrong address, or the server exists but is not ready yet. The browser asked the OS to open a connection, the OS said no, and Cypress reported exactly that. There is no retry that can fix a server that is not there, which is why re-running the spec feels like rolling dice.

This article turns that dice roll into a checklist. You will learn how baseUrl and CYPRESS_BASE_URL decide where visits go, how to prove in ten seconds whether the server is up, and how to wire the readiness gate — start-server-and-test or wait-on — so Cypress physically cannot start before your app answers. Follow it once and this entire failure category disappears from your CI forever.

Why cy.visit() Fails When the Dev Server Is Not Running

When cy.visit() fails, the browser is telling you something precise: it tried to open a connection to a host and port, and the operating system refused it. That refusal has exactly one meaning — no process is accepting connections there right now. It does not mean your test is wrong, your selectors are stale, or Cypress is broken. It means the restaurant is closed and the critic is standing outside an empty building.

The confusion comes from how close everything looks to working. Your code compiles. Your unit tests pass. Cypress launches, the browser opens, and then the very first visit dies. Because the failure appears inside a test runner, your instincts blame the test. Fight that instinct. Open a second terminal while Cypress is failing and run curl -v against the exact URL. If curl also fails, you have proven in five seconds that no test edit can help — the server is simply not there.

This happens most often for embarrassingly simple reasons. You forgot to run npm run dev. You started it in a terminal you later closed. You are on a fresh machine where dependencies were never installed so the server crashed on boot. Or CI started Cypress and the server at the same moment and Cypress won the race. Every one of these is an environment problem wearing a test-failure costume, and the fix is to confirm the server before you ever read the spec.

cypress.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// cypress.config.js — one baseUrl, overridable per environment
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    // Every cy.visit('/') resolves against this origin
    baseUrl: 'http://localhost:3000',
    // Fail fast instead of hanging on a dead server
    defaultCommandTimeout: 8000,
    setupNodeEvents(on, config) {
      // Log the resolved URL so CI output shows where visits go
      console.log(`Cypress baseUrl: ${config.baseUrl}`)
      return config
    },
  },
})

// Terminal: override without editing the file
// CYPRESS_BASE_URL=https://staging.example.com npx cypress run
Try it live
📊 Production Insight
On-call engineers lose the most time when they debug the wrong layer. A five-second curl check at the start of every visit-failure investigation separates server problems from test problems before anyone opens a spec file.
🎯 Key Takeaway
A refused connection means nothing listens at that address — prove it with curl before touching any test code.

baseUrl vs Absolute URLs: How Cypress Resolves cy.visit()

The baseUrl setting in cypress.config.js is the prefix Cypress prepends to every relative visit. With baseUrl: 'http://localhost:3000', calling cy.visit('/login') navigates to http://localhost:3000/login. Call cy.visit('http://other-host:4000/x') with a full URL and baseUrl is ignored entirely for that visit. Most wrong-address failures are developers mixing these two modes without realizing which rule applies.

Ports drift silently as projects evolve. Create React App defaulted to 3000, Vite defaults to 5173, Angular to 4200 — and any of them can move when the port is busy or a config changes. If your specs hard-code cy.visit('http://localhost:3000/...') in twenty places while the app now boots on 5173, you get a suite where some specs pass (the relative ones) and others fail (the absolute ones), which looks like flakiness but is really just stale addresses.

The durable fix is a single source of truth: one baseUrl in config, relative visits everywhere, and absolute URLs only where a test genuinely targets a second service. When a visit fails, diff the config value against the address your server prints on boot — host and port, character by character. Nine times out of ten the mismatch is staring at you from those two lines.

cypress/e2e/visit-resolution.cy.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
// Cypress resolves relative visits against baseUrl
// baseUrl: 'http://localhost:3000'

cy.visit('/')           // -> http://localhost:3000/
cy.visit('/login')      // -> http://localhost:3000/login
cy.visit('dashboard')   // -> http://localhost:3000/dashboard

// Absolute URLs IGNORE baseUrl completely
cy.visit('http://localhost:4000/admin') // goes to :4000, always

// Debug helper: print where you are actually going
cy.visit('/login')
cy.url().should('eq', 'http://localhost:3000/login')
Try it live
📊 Production Insight
Suites that hard-code origins in every spec break on every port change and every new environment. Centralize the origin once and environment differences become a one-variable problem.
🎯 Key Takeaway
One baseUrl in config plus relative visits everywhere — absolute URLs bypass baseUrl and rot silently.

CYPRESS_BASE_URL and Per-Environment Overrides

The CYPRESS_BASE_URL environment variable overrides the baseUrl from your config file for a single run, with no edits and no commits. Run CYPRESS_BASE_URL=https://staging.example.com npx cypress run and every relative visit now targets staging. This is the mechanism that lets one suite run against local, preview, staging, and production simply by changing the environment around it.

It only works if your specs cooperate. Any absolute URL in a spec bypasses baseUrl and therefore ignores the override — a suite full of cy.visit('http://localhost:3000/...') calls will keep hitting localhost no matter what the variable says. That is the classic reason staging runs mysteriously test a developer's laptop address. Audit for absolute visits before you trust environment overrides.

The second trap is assuming the variable exists where it matters. It is set on your laptop but missing in CI, or set in CI but missing in a teammate's shell, and suddenly identical commits behave differently per machine. Treat CYPRESS_BASE_URL like any other deployment input: declare it in each CI environment explicitly, echo its value at the start of the job, and assert in a support file that Cypress.config('baseUrl') is defined. A missing URL should fail the job in one line at second zero, not as forty cryptic visit errors.

📊 Production Insight
Environment-specific failures that nobody can reproduce locally are usually missing env vars, not broken code. Echo every URL input at job start so the logs always show what the suite aimed at.
🎯 Key Takeaway
CYPRESS_BASE_URL overrides baseUrl per run — but only relative visits honor it, and it must exist in every environment.

Wait for Readiness: start-server-and-test and wait-on

Starting a server and waiting for it are two different events, and the gap between them is where flaky cy.visit() failures live. Frameworks like Vite and Next.js compile bundles, load routes, and bind the port in stages — a process can exist for 30 seconds before its first HTTP response. Launch Cypress alongside the server with a bare & or concurrently and you are racing the compiler on every run.

The fix is a readiness gate: a tool that polls the URL and only launches Cypress after it answers. start-server-and-test is the standard choice — it starts your dev command, waits for the resource, runs Cypress, then cleans up the server. wait-on is the lighter alternative when something else owns the server lifecycle and you only need the waiting step. Either one converts a timing race into a guaranteed ordering.

Note what the gate waits on: the HTTP resource, not the process. Polling http://localhost:3000 proves the port accepts connections and the app responds. Watching for a PID or a log line proves far less — a process can print cheerful startup banners minutes before it serves traffic. If your CI still flakes after adding the gate, check that you are polling the same host and port Cypress visits; gating on 127.0.0.1 while visiting localhost (or vice versa) re-opens the race through a DNS-shaped hole.

package.jsonJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// package.json — Cypress cannot start before the app answers
{
  "scripts": {
    "dev": "vite --port 3000",
    "cy:run": "cypress run",
    "test:e2e": "start-server-and-test dev http://localhost:3000 cy:run"
  },
  "devDependencies": {
    "start-server-and-test": "^2.0.0"
  }
}

// What start-server-and-test does, step by step:
// 1. runs `npm run dev`
// 2. polls http://localhost:3000 until it responds
// 3. runs `npm run cy:run`
// 4. shuts the dev server down afterwards

// One-off equivalent without the package:
// npx wait-on http://localhost:3000 && npx cypress run
Try it live
⚠ Process Up Does Not Mean Port Open
A spawned process is not a ready server. Frameworks compile, load routes, then bind the port — Cypress can arrive during that gap. Always gate on the URL responding, never on the process existing.
📊 Production Insight
Every team that deletes its visit flakes traces the fix to one line in package.json. Readiness gating is the highest-leverage single change in this entire article.
🎯 Key Takeaway
Gate Cypress on the URL responding with start-server-and-test or wait-on — never on the server process existing.

CI Failures: Ports, Hosts, and Health Checks

CI amplifies every local sloppiness because the machine is cold, slow, and shared. Dependencies install from scratch, the framework compiles with an empty cache, and Cypress starts the instant the previous step exits. A suite that passes on your warm laptop can fail every CI run simply because boot takes 40 seconds there and 4 seconds at your desk. The failure looks identical to a broken app, which sends the team chasing ghosts.

Harden the CI job in three layers. First, bind the host explicitly — --host 0.0.0.0 — so the server is reachable however the runner routes traffic, and log the bound address so the job output proves where it listens. Second, gate with wait-on plus a generous timeout (cold boots are slow) so Cypress cannot start early. Third, add a curl --fail step between the gate and Cypress: if the app itself is crashing on boot, you get a one-line network error naming the URL instead of forty failing specs.

Keep ports pinned per project and document them where the team will actually see them — the README and the workflow file, not tribal memory. When Docker enters the picture, verify the container port mapping (-p 3000:3000) and remember that localhost inside a container is not the host's localhost. Most Docker visit failures are fixed by binding 0.0.0.0 in the container and visiting the mapped host port from Cypress.

.github/workflows/e2e.ymlJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# .github/workflows/e2e.yml — health-check before Cypress
jobs:
  e2e:
    runs-on: ubuntu-latest
    env:
      CYPRESS_BASE_URL: http://localhost:3000
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Start app in background
        run: npm run dev -- --host 0.0.0.0 --port 3000 &
      - name: Wait for readiness
        run: npx wait-on http://localhost:3000 --timeout 120000
      - name: Prove the app answers (fails fast with a clear log)
        run: curl --fail http://localhost:3000/ -o /dev/null
      - name: Run Cypress
        run: npx cypress run
Try it live
📊 Production Insight
A dedicated health-check step is the cheapest observability you can buy: it turns forty confusing spec failures into one line that names the dead URL.
🎯 Key Takeaway
Cold CI boots slowly — bind the host explicitly, gate with wait-on, and curl before Cypress runs.

A Startup Checklist That Ends cy.visit Flakes

End this failure category with a checklist you run once and then encode into scripts so nobody relies on memory. Confirm the dev server boots cleanly with npm run dev and loads in a real browser at the documented URL. Confirm baseUrl in cypress.config.js matches that URL exactly, and that every spec uses relative visits. Confirm CYPRESS_BASE_URL is set in each CI environment and echoed at job start. Confirm the launch path goes through start-server-and-test or wait-on, never a bare background &. Confirm a curl health check sits between readiness and Cypress.

Then make the setup self-defending. Assert baseUrl in cypress/support/e2e.js so a missing config throws a named error before the first test. Log the resolved baseUrl via setupNodeEvents so every CI run records what the suite aimed at. Pin the port per project and reject drive-by changes to it in code review — a port change without updating config and docs is a guaranteed red pipeline.

Do this and cy.visit() failures stop being mysteries. When a visit fails in the future, the checklist tells you in seconds whether the server died (health check red), the address moved (logs disagree), or the app genuinely broke (server up, page down) — three different owners, three different fixes, zero guessing.

📊 Production Insight
Checklists that live in wikis rot; checks that live in scripts endure. Every manual verification in this article has a scripted equivalent — use it.
🎯 Key Takeaway
Encode the checklist into scripts and assertions so correct ordering is automatic, not remembered.
● Production incidentPOST-MORTEMseverity: high

The Afternoon Every Cypress Job Failed Because the Dev Server Was Still Booting

Symptom
All 40 Cypress specs failed at the first cy.visit() with connection-refused errors. Re-running the job sometimes helped — a classic race signature. The application logs showed successful boot messages timestamped after the Cypress failures, proving the tests had arrived too early.
Assumption
The team assumed concurrently "npm run dev" "cypress run" was safe because both processes start together. Nobody realized process-spawn and port-listening are different events, or that a cold CI machine boots the framework far slower than a warm laptop.
Root cause
The CI job launched the Vite dev server and Cypress simultaneously with concurrently. On a cold runner, Vite needed 20-40 seconds to compile and bind port 5173, while Cypress started visiting within 5 seconds. Every cy.visit() hit a closed port and failed with ECONNREFUSED. Local runs passed because the developer's server was already warm from hours of coding.
Fix
They replaced the launch line with start-server-and-test 'npm run dev' http://localhost:3000 'cypress run', added a curl --fail health-check step before it, and logged the server's bound address on boot. The suite went from 30% red to green on every run that week, and the health check now names the culprit in seconds on the rare occasion the server itself crashes.
Key lesson
  • Process-started is not port-listening. Gate test runners on the HTTP resource responding, never on the server process spawning.
  • A red E2E job should first prove the app is reachable with curl before anyone reads a spec file — most visit failures are infrastructure, not tests.
  • Log the server's bound address on every boot. When local, Docker, and CI disagree about hosts and ports, that one log line ends the argument.
Production debug guideFive checks that separate a dead server from a wrong address in under two minutes.5 entries
Symptom · 01
cy.visit() fails immediately with ECONNREFUSED or ERR_CONNECTION_REFUSED
→
Fix
Leave Cypress failing, open a second terminal, and run curl -v http://localhost:3000/ (your exact visit URL). Connection refused confirms nothing listens there. Then run lsof -i :3000 (macOS/Linux) to see if any process holds the port. If the port is empty, start your dev server (npm run dev) and re-run — do not touch the spec.
Symptom · 02
curl succeeds on one port, but Cypress visits a different one
→
Fix
Print the resolved config with npx cypress info and open cypress.config.js. Compare the baseUrl value character by character with the address your server log prints on boot (host and port). If they differ, fix baseUrl — or export CYPRESS_BASE_URL to match the server — then confirm with curl before re-running Cypress.
Symptom · 03
The same commit passes and fails at random, mostly in CI
→
Fix
Check for a readiness gate in package.json scripts. If Cypress launches with plain & backgrounding or concurrently with no URL check, replace it with start-server-and-test 'npm run dev' http://localhost:3000 'cypress run'. As a one-off proof, run npx wait-on http://localhost:3000 && npx cypress run and watch the flake vanish.
Symptom · 04
Passes locally, fails in CI (or the reverse) with identical code
→
Fix
Run printenv | grep -i cypress in the CI job to see the actual value, and echo Cypress.config('baseUrl') from a support file. Set CYPRESS_BASE_URL explicitly in each CI environment's variables (staging jobs get the staging URL). Never rely on a .env file that only exists on your laptop.
Symptom · 05
Works on your machine but fails inside Docker or a remote CI runner
→
Fix
Check what the server bound to: look for Local: http://localhost:3000 vs Network: http://172.x.x.x in its boot log. If Cypress runs in a different container, bind with --host 0.0.0.0 and verify Docker's -p 3000:3000 mapping. Add curl --fail $CYPRESS_BASE_URL as a CI step before Cypress so a mapping mistake fails with a clear message.
cy.visit() Failures — Root Cause vs Fix at a Glance
Root CauseHow to ConfirmFixPrevention
No server is listening on the visit URLRun curl -v <url> while Cypress fails; connection refused means nothing is thereStart the dev server, then re-run CypressLaunch Cypress through start-server-and-test
baseUrl points at the wrong port or hostcurl succeeds on one port but Cypress targets another; compare cypress.config.js with the server logCorrect baseUrl or pass CYPRESS_BASE_URLOne config-owned port per project; fail fast if baseUrl is unset
Server process is up but not ready yetFailure is intermittent; a manual retry passes once boot finishesGate Cypress on wait-on http://localhost:3000Always wait on the HTTP resource, never on process spawn
Environment URL mismatch between local and CILocal run passes, CI run fails (or the reverse) with the same commitExport CYPRESS_BASE_URL per environmentAssert baseUrl in a support file before the first test
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
cypress.config.jsconst { defineConfig } = require('cypress')Why cy.visit() Fails When the Dev Server Is Not Running
cypresse2evisit-resolution.cy.jscy.visit('/') // -> http://localhost:3000/baseUrl vs Absolute URLs
package.json{Wait for Readiness
.githubworkflowse2e.ymljobs:CI Failures

Key takeaways

1
cy.visit() fails with ECONNREFUSED when nothing listens at the URL
it is a server problem, not a test problem.
2
baseUrl prefixes every relative visit; CYPRESS_BASE_URL overrides it per run without editing config.
3
Process-started is not port-listening
gate Cypress on HTTP readiness with wait-on or start-server-and-test.
4
Prove it in seconds
curl the exact URL Cypress visits while the failure reproduces.
5
In CI, bind the host explicitly, log the bound address, and health-check before Cypress starts.
6
Assert baseUrl in a support file so a missing config fails fast with a named error.

Common mistakes to avoid

5 patterns
×

Running Cypress before starting the dev server

Symptom
cy.visit() fails instantly with ECONNREFUSED, and you waste time re-reading the spec when the app was never up.
Fix
Run npm run dev (or your framework's equivalent) in one terminal and confirm the page loads in a real browser before you ever open Cypress. Better yet, encode the ordering in npm scripts with start-server-and-test so the mistake becomes impossible.
×

Hard-coding localhost:3000 in every spec

Symptom
Half the suite points at port 3000 while the app moved to 5173 (Vite) or 4200 (Angular), so some specs pass and others fail with connection errors.
Fix
Set baseUrl: 'http://localhost:3000' (your real port) in cypress.config.js and call cy.visit('/') with relative paths. If you must use absolute URLs, keep them in one config value so there is a single place to update.
×

Assuming the server is ready because the process started

Symptom
Flaky CI: the same commit passes and fails at random because Cypress sometimes wins the race against a cold-booting dev server.
Fix
Use start-server-and-test 'npm run dev' http://localhost:3000 'cypress run' or npx wait-on http://localhost:3000 before launching Cypress. Process-is-running does not mean port-is-listening.
×

Setting CYPRESS_BASE_URL in one environment and forgetting the rest

Symptom
Tests pass locally but hit the wrong host in CI — or vice versa — because the env var is only defined on your laptop.
Fix
Export CYPRESS_BASE_URL in each environment's CI config (staging URL for staging jobs, preview URL for preview jobs) and assert it at the top of a support file: if (!Cypress.config('baseUrl')) throw new Error('baseUrl is missing').
×

Letting the port or host drift between local, Docker, and CI

Symptom
Works on your machine, ECONNREFUSED in Docker or CI, because the server bound to 127.0.0.1 inside a container Cypress cannot reach.
Fix
Bind explicitly (vite --host 0.0.0.0 --port 3000, or Docker port mapping -p 3000:3000) and keep one documented port per project. Log the bound address on boot so CI output shows where the server actually listens.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Your Cypress run fails on cy.visit('/') with ECONNREFUSED. What is the f...
Q02JUNIOR
What is baseUrl in Cypress and how do you override it per environment?
Q03SENIOR
Cypress and the dev server start together in CI, but visits still fail i...
Q04SENIOR
cy.visit() works locally but fails inside Docker with the same config. H...
Q05SENIOR
How would you design a Cypress setup so a dead dev server can never prod...
Q01 of 05JUNIOR

Your Cypress run fails on cy.visit('/') with ECONNREFUSED. What is the first thing you check?

ANSWER
The visit fails with a connection error such as ECONNREFUSED because no process is listening on that host and port. First I would curl the URL to confirm, then start the dev server, then re-run. For a permanent fix I would gate Cypress behind start-server-and-test so the ordering is enforced, not remembered.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What does ECONNREFUSED mean when cy.visit() fails?
02
Does CYPRESS_BASE_URL work if my specs use full URLs?
03
Can I run Cypress without starting my dev server?
04
How do I point one Cypress run at staging without editing config?
05
Why does cy.visit() pass locally but fail in CI?
06
start-server-and-test vs wait-on — which should I use?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's Cypress. Mark it forged?

6 min read · try the examples if you haven't

←
Previous
Selenium TimeoutException in Headless but Not Headed
1 / 4 · Cypress
Next
Cypress Element Detached From DOM
→