ScreenshotNeo

BlogHow-to

How to Wait for an App to Start Before Running Cypress Tests

Start your app before Cypress, wait for its URL to respond, then run tests. Compare reliable startup options and handle browser and app readiness separately.

By the ScreenshotNeo team4 October 20269 min read

Start the application server before Cypress, wait until its URL responds, then run Cypress. For an npm project where one command should manage startup, readiness, tests, and cleanup, use start-server-and-test. If you already manage the server process yourself, wait on its URL with wait-on before invoking Cypress. Set Cypress’s baseUrl as well: it tells Cypress where the app lives, but does not start the server.

Do not rely on npm start & npx cypress run or a fixed sleep. The first can race ahead before the server is ready; the second waits for a duration rather than checking whether the app can respond. Cypress recommends starting the web server before running Cypress. Cypress CI guidance · Cypress best practices.

This option is a good default for local development and CI when the test command should start the app, wait for it, run Cypress, and shut down the server. From the project directory, install the utility as a development dependency:

npm install --save-dev start-server-and-test

Add scripts to package.json. Adapt the server command, port, URL, and Cypress command to your project:

{
  "scripts": {
    "start": "my-server -p 3030",
    "cy:run": "cypress run",
    "test:e2e": "start-server-and-test start http://localhost:3030 cy:run"
  }
}

Run the complete sequence with:

npm run test:e2e

The utility starts the named server command, waits for the supplied URL to return HTTP 200, runs the test command, and shuts down the server after the tests finish. Use the URL that represents the app Cypress actually needs to visit, not just an unrelated process health endpoint. See the Cypress CI examples and notes.

When the server does not accept HEAD requests

Some development servers do not answer the default readiness request as expected. Specify GET explicitly with the http-get:// URL form:

{
  "scripts": {
    "start": "my-server -p 3030",
    "cy:run": "cypress run",
    "test:e2e": "start-server-and-test start http-get://localhost:3030 cy:run"
  }
}

For local HTTPS with a development certificate

If the local server uses HTTPS and its development certificate is not trusted, Cypress documents an explicit GET form and the START_SERVER_AND_TEST_INSECURE=1 setting for this local case:

{
  "scripts": {
    "start": "my-server -p 3030 --https",
    "cy:run": "cypress run",
    "test:e2e": "START_SERVER_AND_TEST_INSECURE=1 start-server-and-test start https-get://localhost:3030 cy:run"
  }
}

Keep this exception scoped to local development with a known development certificate. Do not use it to weaken certificate verification for a general production endpoint. On Windows, use a cross-platform environment-variable approach if your shell does not support the assignment syntax shown above.

2. Set Cypress baseUrl and run tests against the app

Configure baseUrl in cypress.config.js or cypress.config.ts. This lets tests use relative paths and allows Cypress to check that the configured app URL is reachable. External startup orchestration is still required.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3030',
  },
})

A test can now visit the root path without repeating the host:

// cypress/e2e/home.cy.js
describe('home page', () => {
  it('loads the app', () => {
    cy.visit('/')
    cy.contains('Welcome').should('be.visible')
  })
})

Run this test through the npm run test:e2e wrapper above. Setting baseUrl alone does not launch a server. Cypress’s baseUrl guidance explains how relative cy.visit() and cy.request() URLs are resolved.

3. If you manage the process yourself, wait with wait-on

wait-on is useful when another script, CI job, or process manager already owns server startup and shutdown. Install it if needed:

npm install --save-dev wait-on

For a Unix-like shell, a minimal sequence is:

npm start &
npx wait-on http://localhost:3030
npx cypress run

For a local script where you must clean up the server yourself, retain its process ID and stop it even when Cypress fails:

#!/usr/bin/env bash
set -euo pipefail

npm start &
server_pid=$!

cleanup() {
  kill "$server_pid" 2>/dev/null || true
  wait "$server_pid" 2>/dev/null || true
}
trap cleanup EXIT

npx wait-on http://localhost:3030
npx cypress run

Adapt the process command if it launches child processes that outlive the shell; stopping only the parent may not stop every child. CI providers commonly clean up background processes, but local scripts may need explicit PID handling. If process lifecycle management is not already solved, the wrapper in section 1 is simpler. Cypress describes both approaches in its CI documentation.

4. Use Cypress GitHub Action startup options in GitHub Actions

When running in GitHub Actions, the Cypress action provides start and wait-on options, so the workflow can manage startup and readiness without installing an extra readiness package. Keep the server command and URL aligned with your project. The action’s official documentation includes workflow examples: Cypress GitHub Action.

name: Cypress
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - uses: cypress-io/github-action@v6
        with:
          start: npm start
          wait-on: 'http://localhost:3030'
          browser: chrome

Adjust action and runtime versions to the versions your repository uses. If your workflow already builds the app or starts it in a preceding job, avoid starting a duplicate server; configure the action for your actual lifecycle.

5. Know which kind of readiness you are waiting for

“The app is ready” can refer to several different conditions. Use a wait that matches the condition you need.

Condition What to use What it does not prove
Server can serve HTTP start-server-and-test, wait-on, or GitHub Action wait-on That client-side app initialization or every API request has finished.
Cypress can reach the configured app URL baseUrl in Cypress config That it starts the server process.
Browser navigation has loaded cy.visit() That arbitrary background XHR requests or app-specific initialization are finished.
App-specific initialization is complete A meaningful app readiness signal asserted in the browser That unrelated server processes or future requests are ready.
Specific API responses have arrived cy.intercept() aliases and cy.wait() That every possible network request has completed.

Wait for application initialization after visiting

A server may return HTML while a client-side app is still initializing. Expose a property only when the app reaches a meaningful ready state, then assert it in Cypress:

// App code: set this after the app has completed the work the test needs.
if (window.Cypress) {
  window.appReady = true
}

// Cypress spec
beforeEach(() => {
  cy.visit('/')
  cy.window().should('have.property', 'appReady', true)
})

Place the signal after the relevant initialization, such as loading required configuration or mounting the application. Setting it immediately on initial script execution only proves that the script ran. Cypress documents this pattern in the cy.window() API.

Wait for specific network requests

When a test depends on a particular API response, register the intercept before the visit, then wait for that alias:

cy.intercept('GET', '/api/products').as('products')
cy.visit('/')
cy.wait('@products')
cy.get('[data-cy="product-list"]').should('be.visible')

Cypress cannot infer that every arbitrary XHR or Ajax request in an app is finished. Define the requests that matter to the test and assert their outcomes. For Cypress navigation behavior and visit timeout configuration, see the cy.visit() API and the Cypress FAQ.

6. Troubleshooting common startup failures

Symptom Likely cause Fix
Cypress starts before the server responds The server was backgrounded and Cypress was invoked immediately. Put a URL readiness check between startup and Cypress. Use start-server-and-test, wait-on, or the GitHub Action’s wait-on.
The readiness wait never succeeds Wrong port, host, protocol, route, server command, or a server that never became healthy. Run the start command alone, inspect its logs, and request the exact readiness URL from the same environment where Cypress runs.
Server is usable in a browser but readiness fails The server may reject HEAD or return a non-200 status on the checked URL. Try http-get://localhost:3030 for an explicit GET when using start-server-and-test. Choose a route that returns HTTP 200 for a healthy app.
HTTPS readiness fails on a local certificate The local certificate is not trusted by the readiness client. For the documented local-development case, use https-get:// and START_SERVER_AND_TEST_INSECURE=1. Keep the workaround local.
Port already in use An earlier server is still running or another process owns the port. Stop the old process, select a free port, and ensure the server command and baseUrl use the same port. Prefer managed cleanup.
URL check passes, but the page is still not ready HTTP readiness only proves a response, not client-side initialization or data loading. Wait for an app readiness signal or the specific intercepted API request that the test needs.
cy.visit() times out The browser navigation did not reach its expected load condition, or the page is slow or stuck. Check browser console and server logs, verify the URL, and investigate slow or blocked resources. Increase the visit timeout only when a genuinely slower navigation is expected.
Local tests leave a server behind A background process was not shut down after Cypress exited. Use start-server-and-test or trap script exit and terminate the retained PID. Do not depend on a Cypress after hook for process cleanup.

7. Performance, reliability, and cost

  • Prefer readiness over fixed delays. A fixed delay can waste time when startup is fast and still fail when startup is slow. A URL probe proceeds when its condition is met.
  • Probe the right endpoint. A root route that redirects, requires authentication, or returns an error may not be an appropriate health check. Pick a URL and method that produce the success response your readiness tool expects.
  • Keep one owner for lifecycle. Let a wrapper, the CI action, or your script own startup and shutdown. Multiple owners can create duplicate servers and port conflicts.
  • Separate infrastructure from browser waits. Wait for the server before Cypress starts, then use Cypress assertions for app state and specific requests. This makes failures easier to diagnose.
  • Do not tune Cypress timeouts to mask startup races. The Cypress FAQ documents a 60,000 ms default for cy.visit() and a 4-second default for commands generally; those defaults govern browser commands, not external server startup readiness. Adjust a timeout only for a measured, expected slow operation.
  • Cost. The readiness tools discussed here are software packages or CI configuration. Account for CI execution time and any infrastructure your app requires; no universal runtime or cost figure applies.

8. Choosing the workflow

Workflow Choose it when Cleanup
start-server-and-test You want one npm command to start, wait, test, and stop. Handled by the wrapper after the test command.
wait-on Your own script or process manager already controls the server. Handle the process yourself, especially for local runs.
Cypress GitHub Action start and wait-on Your tests run in GitHub Actions and you want startup in the action workflow. Managed by the workflow environment and action.

Or skip the browser setup

If you need website screenshots alongside browser testing or debugging, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request captures a URL as PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each step can be turned off.
  • Bot checks, blank pages, timeouts, and failed loads are never billed; response headers identify the page verdict and billing status. Cache hits are also not billed.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card required.

FAQ

Does setting baseUrl start the application?

No. It configures Cypress’s app URL and enables its reachability checks. Start the server with a wrapper, CI action, or script first.

Should I start the server in a Cypress task or hook?

No. Cypress advises starting the web server before Cypress. A long-running process does not fit the task lifecycle, and an after hook is not guaranteed to run.

Does cy.visit wait until all app requests have finished?

No. It waits for browser navigation and the page’s load event. For a specific API call, register a cy.intercept() alias and wait on it; for client initialization, assert an app-specific readiness condition.

Can I replace the URL check with a longer sleep?

A longer sleep still does not establish readiness. Wait for the URL or resource that Cypress needs to be available.