Resource library

QA How-To

How to Wait for an API response in Cypress (2026)

Learn cypress how to wait for an API response with cy.intercept aliases, response assertions, stubs, GraphQL waits, multi-request flows, and flake-free CI patterns.

24 min read | 2,628 words

TL;DR

Use `cy.intercept(method, url).as('alias')` before the UI action, then `cy.wait('@alias')` before assertions. Inspect `response.statusCode` and body when needed, and reserve fixed sleeps for nothing in normal application testing.

Key Takeaways

  • Register `cy.intercept` before the request, alias it, then `cy.wait('@alias')`.
  • Use precise method and path matchers to avoid waiting on the wrong call.
  • Assert response status and key body fields when correctness depends on the server.
  • Stub edge states deliberately while keeping critical paths integrated.
  • Wait for multiple aliases when a page composes several dependencies.
  • Handle GraphQL by aliasing operation names inside route handlers.
  • Replace millisecond sleeps; they hide races and slow suites.

The reliable answer to cypress how to wait for an API response is: declare cy.intercept() before the action that triggers the request, give the route an alias with .as('name'), perform the action, then synchronize with cy.wait('@name') before asserting UI that depends on that response. This pattern replaces fixed cy.wait(5000) sleeps and ties the test to the network contract the app actually uses.

Waiting on the wrong signal is one of the top sources of flaky Cypress suites. DOM text can appear from cache, a previous visit, or optimistic UI while the real request is still in flight. This guide covers intercept setup, alias waits, response assertions, static and dynamic stubs, request sequencing, GraphQL considerations, timeouts, and anti-patterns that hide product bugs.

TL;DR

Situation Pattern Notes
Wait for a real backend call intercept -> action -> wait('@alias') Assert UI after the wait
Assert status/body cy.wait('@alias').its('response.statusCode') Or use .then
Stub a JSON response intercept(url, { fixture }) Deterministic UI states
Match one of many calls Narrow route or use routeHandler logic Avoid over-broad **
Several ordered calls Wait on each alias in order Name them clearly
No network needed Prefer app callbacks only if product guarantees them Still often pair with intercepts

If you remember only one rule: register the intercept before the request can fire.

1. Cypress How to Wait for an API Response: Core Pattern

it('shows projects returned by the API', () => {
  cy.intercept('GET', '/api/projects').as('listProjects')

  cy.visit('/projects')
  cy.wait('@listProjects')

  cy.get('[data-cy=project-row]').should('have.length.at.least', 1)
})

Why this order matters:

  1. cy.intercept installs the route listener first.
  2. cy.visit or a click triggers the application request.
  3. cy.wait('@listProjects') yields when Cypress observes a matching request and its response complete according to the command behavior.
  4. UI assertions run against a settled network-dependent state.

If you visit first and intercept second, you can miss the request entirely. That mistake produces timeouts that look like backend failures but are really test setup bugs.

Use stable, intention-revealing alias names: @createInvoice, @saveProfile, @searchUsers. Avoid @get and @req1.

2. Choose Precise Route Matchers

Over-broad matchers wait on the wrong call. Prefer method + path patterns that match your app:

cy.intercept('GET', '/api/projects').as('listProjects')
cy.intercept('GET', '/api/projects/*').as('projectDetail')
cy.intercept('POST', '/api/projects').as('createProject')

You can also match with a RouteMatcher object:

cy.intercept({
  method: 'GET',
  pathname: '/api/search',
  query: { q: 'cypress' },
}).as('searchCypress')

When the client adds cache busting query strings, match on pathname or use a minimatch pattern that ignores irrelevant params. When multiple tabs or components hit similar endpoints, narrow by pathname segments or required headers.

Avoid intercepting every request with ** unless you are debugging. Global intercepts increase flake and make waits ambiguous.

3. Assert Response Status, Headers, and Body

cy.wait('@alias') yields an interception object you can inspect:

cy.wait('@createProject').then(({ request, response }) => {
  expect(request.body).to.include({ name: 'Atlas' })
  expect(response?.statusCode).to.eq(201)
  expect(response?.body).to.have.property('id')
})

Chained forms work for simple checks:

cy.wait('@listProjects').its('response.statusCode').should('eq', 200)

Asserting the response matters when the UI might show stale data. A green list does not prove the latest GET succeeded if the UI reused in-memory state. Pair network assertions with one user-visible outcome.

Be careful with large bodies in assertion messages. Check specific fields, not entire payloads, especially when payloads include timestamps or noisy arrays.

4. Stub Responses for Deterministic UI States

Waiting on a real environment is valuable for integration confidence. Stubbing is valuable for edge UI states that are hard to seed:

cy.intercept('GET', '/api/projects', {
  statusCode: 200,
  body: {
    items: [
      { id: 'p1', name: 'Alpha', status: 'active' },
      { id: 'p2', name: 'Beta', status: 'archived' },
    ],
  },
}).as('listProjects')

cy.visit('/projects')
cy.wait('@listProjects')
cy.contains('[data-cy=project-row]', 'Alpha').should('be.visible')
cy.contains('[data-cy=project-row]', 'Beta').should('be.visible')

Fixture-based stubs keep specs clean:

cy.intercept('GET', '/api/projects', {
  fixture: 'projects/list.json',
}).as('listProjects')

Use stubs for empty states, 403 pages, 500 banners, and pagination shapes. Keep at least some critical-path tests on real backends or shared ephemeral environments so contract drift is still detected. For deeper stubbing tactics, see Cypress network stubbing.

5. Dynamic Route Handlers and Conditional Replies

Route handlers let you inspect the request and reply dynamically:

cy.intercept('POST', '/api/projects', (req) => {
  expect(req.body).to.have.property('name')
  req.reply({
    statusCode: 201,
    body: { id: 'p_new', name: req.body.name, status: 'active' },
  })
}).as('createProject')

You can also continue the request to the real server after assertions:

cy.intercept('POST', '/api/projects', (req) => {
  expect(req.headers).to.have.property('authorization')
  req.continue()
}).as('createProject')

Use req.continue() when you want spy-like observation without stubbing. Use req.reply() when you need a controlled body. Do not mix the two accidentally in ways that double-send.

Handlers are also useful for delaying a response to test pending UI, when your Cypress version and API support delay options on the reply. Prefer documented delay mechanisms over lab-only hacks.

6. Waiting for Multiple API Responses

Pages often fire several requests. Wait for each dependency before asserting the composed UI:

cy.intercept('GET', '/api/projects').as('projects')
cy.intercept('GET', '/api/me').as('me')
cy.intercept('GET', '/api/notifications').as('notifications')

cy.visit('/home')
cy.wait(['@me', '@projects', '@notifications'])

cy.get('[data-cy=home-ready]').should('be.visible')

Array waits are convenient when order is unimportant. When order matters for the product, wait sequentially and name each step:

cy.get('[data-cy=checkout]').click()
cy.wait('@createCheckoutSession')
cy.wait('@fetchPaymentMethods')
cy.get('[data-cy=pay]').should('be.enabled')

If one request is optional, do not hard-wait for it in every run. Optional analytics calls should not gate functional assertions.

7. GraphQL, Search, and High-Frequency Endpoints

GraphQL often uses one URL for many operations. Match by operation name in the body or a custom header your client sends:

cy.intercept('POST', '/graphql', (req) => {
  const body = req.body
  if (body && body.operationName === 'ListProjects') {
    req.alias = 'gqlListProjects'
  }
})

cy.visit('/projects')
cy.wait('@gqlListProjects')

Assigning req.alias inside a handler is a common pattern for conditional GraphQL waits. Keep operation names stable with your client.

Search endpoints may fire on every keystroke. Waiting on @search after typing "cypress" can catch an intermediate request for "c" or "cy". Strategies:

  • Debounce in the app and type with realistic delay, then wait.
  • Match the final query string exactly.
  • Click a Search button if the product has an explicit submit.
  • Stub and assert the last request in a handler that records calls.

High-frequency polling is similar. Prefer waiting for a specific response body condition through retries on the UI, or intercept and wait for a response that indicates completion, rather than waiting N times in a loop without a stop condition.

8. Timeouts, Failures, and Debugging Missed Waits

Default command timeouts may be too low for slow environments and too high for fast unit-like stubs. Set a local timeout on a wait when the operation has a known upper bound:

cy.wait('@generateReport', { timeout: 30000 })

When a wait fails, Cypress reports that the alias never matched. Debug by:

  1. Confirming the intercept was registered before the action.
  2. Checking method and path against the real request in the Command Log.
  3. Verifying the app actually fired the request (auth redirect can cancel it).
  4. Ensuring the matcher is not too strict on query or headers.
  5. Checking for multiple Cypress domains or cy.origin boundaries that change where requests are observed.

Do not "fix" a missed wait by inserting cy.wait(10000). That hides the matcher bug and slows the suite.

If the request fires twice, cy.wait('@alias') consumes one interception. A second wait can target the next. If you needed the second call only, narrow the matcher or reset state so only one call occurs.

9. Optimistic UI, Caching, and Race Conditions

Modern UIs optimistically update before the server confirms. A test that only checks the optimistic DOM can pass while the API fails. Pattern:

cy.intercept('POST', '/api/todos', {
  statusCode: 500,
  body: { message: 'nope' },
}).as('createTodo')

cy.get('[data-cy=new-todo]').type('Buy milk{enter}')
cy.wait('@createTodo')
cy.contains('[data-cy=error]', 'could not save').should('be.visible')

For successful optimistic flows, still wait on the confirming request before asserting persistence after reload:

cy.get('[data-cy=new-todo]').type('Buy milk{enter}')
cy.wait('@createTodo').its('response.statusCode').should('eq', 201)
cy.reload()
cy.intercept('GET', '/api/todos').as('listTodos')
cy.visit('/todos')
cy.wait('@listTodos')
cy.contains('[data-cy=todo-row]', 'Buy milk').should('be.visible')

Caching layers (service workers, in-memory stores, SWR-style caches) can prevent network calls on second visit. If your test requires a network wait, bust cache or use first-load scenarios. Otherwise assert against the cached UI without a false network wait.

10. cy.request vs Intercept Waits

cy.request talks to the server outside the browser and does not pass through cy.intercept. Use it for test setup, not as a substitute for waiting on UI-triggered calls.

// Setup: seed data outside the UI
cy.request('POST', '/api/test/seed', { projectName: 'Seeded' })

// Exercise: wait on the browser-driven list call
cy.intercept('GET', '/api/projects').as('listProjects')
cy.visit('/projects')
cy.wait('@listProjects')
cy.contains('Seeded').should('be.visible')

This split keeps setup fast and still validates the browser path. More request setup patterns appear in Cypress cy.request for API.

11. Cypress How to Wait for an API Response in CI

In CI, backends are slower and noisier. Practices that help:

  • Register intercepts before navigation.
  • Prefer deterministic stubs for pure UI state tests.
  • Use real waits for a small integration slice against a known environment.
  • Fail on unexpected 5xx for critical aliases when that is a product gate.
  • Keep alias names unique across parallel specs if you log them centrally.
  • Avoid depending on third-party network calls; stub analytics and chat widgets.

Flake triage should inspect whether the wait timed out (request never matched) or the wait passed and a later DOM assertion failed (UI bug or selector issue). Those are different failures and need different fixes. Guidance on flake control pairs well with Cypress handling flaky tests.

12. Anti-Patterns That Create False Confidence

Anti-pattern Why it hurts Better approach
cy.wait(3000) Arbitrary, slow, racey cy.wait('@alias')
Intercept after click Missed request Intercept first
Wait on ** Ambiguous match Narrow matcher
DOM-only assert after mutation Ignores failed POST Wait + response assert
Stub every call in every test Misses contract drift Mix stubbed and real
One alias for unrelated calls Consumes wrong interception Unique aliases

Delete sleeps when you find them. Replace with event-based waits: network, route, or deterministic UI predicates that retry.

13. End-to-End Example: Search, Select, Save

describe('Project rename', () => {
  it('waits for search and save APIs before asserting', () => {
    cy.intercept('GET', '/api/projects/search*').as('searchProjects')
    cy.intercept('PATCH', '/api/projects/*').as('renameProject')

    cy.visit('/projects')
    cy.get('[data-cy=search]').type('Atlas')
    cy.wait('@searchProjects').its('response.statusCode').should('eq', 200)
    cy.contains('[data-cy=project-row]', 'Atlas').click()

    cy.get('[data-cy=project-name]').clear().type('Atlas Prime')
    cy.get('[data-cy=save-project]').click()
    cy.wait('@renameProject').then(({ request, response }) => {
      expect(request.body).to.deep.include({ name: 'Atlas Prime' })
      expect(response?.statusCode).to.eq(200)
    })
    cy.contains('[data-cy=toast]', 'Saved').should('be.visible')
    cy.get('[data-cy=project-name]').should('have.value', 'Atlas Prime')
  })
})

This test synchronizes on the two network boundaries that define correctness. The final DOM checks confirm the user-visible result after those boundaries succeed.

Interview Questions and Answers

Q: How do you wait for an API response in Cypress?

I register cy.intercept with a method and route, alias it, trigger the UI action, then cy.wait('@alias') before assertions that depend on the response.

Q: Why is cy.wait(5000) a problem?

It is not tied to the event you care about. It slows tests when the app is fast and still flakes when the app is slow or the request never fires.

Q: When should you stub instead of hitting the real API?

Stub for deterministic edge UI states and third parties. Keep critical business paths integrated against a real or realistic backend.

Q: How do you wait for GraphQL requests?

Match the shared /graphql endpoint and alias by operation name inside a route handler, then wait on that alias.

Q: What does cy.wait('@alias') return?

An interception object including request and response details you can assert on, such as status codes and bodies.

Q: How do you handle multiple parallel requests on page load?

Alias each required endpoint and cy.wait(['@a', '@b', '@c']) before asserting the composed screen.

Q: How is cy.request different from intercept waits?

cy.request is outside the browser network stack used by the app and bypasses intercepts. It is for setup or direct API checks, not for waiting on UI-triggered calls.

Common Mistakes

  • Registering intercepts after navigation or click.
  • Using sleeps instead of alias waits.
  • Over-broad route patterns that match the wrong call.
  • Asserting UI without confirming a failed response path.
  • Expecting cy.request to be visible to cy.intercept.
  • Waiting on analytics calls that are allowed to fail.
  • Consuming the first interception when the second call mattered.
  • Stubbing 100% of traffic and never detecting backend contract breaks.
  • Ignoring optimistic UI failure handling.
  • Raising global timeouts instead of fixing matchers.

Conclusion

For cypress how to wait for an API response, install a precise cy.intercept, alias it, trigger the action, and cy.wait('@alias') before dependent assertions. Inspect status and body when correctness depends on the server contract, and use stubs deliberately for edge states.

Audit one flaky spec this week: remove fixed waits, add named intercepts, and assert the response that actually gates the UI. That single refactor often turns an intermittent failure into a deterministic, reviewable test.

14. Pattern Library for Common UI Events

Different UI events trigger network calls in different ways. Match the wait to the event.

Initial page load: intercept before cy.visit, wait after visit, then assert page widgets.

Button click: intercept, click, wait, assert toast or navigation.

Form submit: intercept POST/PUT, submit, wait, assert validation vs success branches.

Infinite scroll: intercept page 2 URL or cursor param, scroll the list container, wait, assert item count increased.

Polling job status: intercept the status endpoint, start the job, then wait for a response that contains status: 'done' using a handler that only aliases completion responses, or assert the UI ready state with a timeout that reflects the product SLA.

cy.intercept('GET', '/api/jobs/*', (req) => {
  req.continue((res) => {
    if (res.body?.status === 'done') {
      req.alias = 'jobDone'
    }
  })
})

cy.get('[data-cy=start-job]').click()
cy.wait('@jobDone', { timeout: 60000 })
cy.contains('[data-cy=job-status]', 'Done').should('be.visible')

Adapt this to your Cypress version's recommended continue/alias patterns if they differ slightly, and keep the idea: wait for the meaningful response, not an arbitrary number of polls.

Collaborating With Developers on Testable Network Contracts

QA should negotiate stable contracts that make waits easy:

  • Stable paths and operation names.
  • Documented error shapes for failed saves.
  • Explicit ready markers when many calls compose a page.
  • Avoidance of anonymous fire-and-forget calls that mutate critical UI without a traceable request.

When developers rename endpoints, intercepts should fail loudly. That is desirable. Update aliases in the same pull request as the client change. Treat intercept matchers as first-class test code, not disposable glue.

If the team uses generated API clients, consider centralizing route constants so production code and tests share path fragments. Shared constants reduce matcher drift.

Cypress How to Wait for an API Response: Review Checklist

Before merging a spec that depends on network timing, verify:

  1. Intercept registration order is correct.
  2. Matcher is specific enough.
  3. Alias name is unique and meaningful.
  4. Wait happens before dependent assertions.
  5. Response assertions exist when the server contract matters.
  6. No fixed sleep remains beside the wait "just in case."
  7. Failure paths are covered where users can hit them.
  8. Third-party calls are stubbed or ignored intentionally.

This checklist keeps suites fast and honest. It is also a strong interview talking point when someone asks how you design non-flaky Cypress tests around asynchronous APIs.

Working With Authentication Calls and Token Refresh

Auth traffic can steal your waits if matchers are broad. A page load might call /api/me, refresh tokens, and then load business data. If you alias **/api/**, you may wait on the wrong call.

Prefer explicit business endpoints in functional tests. If the feature under test is login itself, alias login and token endpoints intentionally:

cy.intercept('POST', '/api/auth/login').as('login')
cy.intercept('GET', '/api/me').as('me')

cy.visit('/login')
cy.get('[data-cy=email]').type('qa@example.test')
cy.get('[data-cy=password]').type('correct-horse-battery{enter}')
cy.wait('@login').its('response.statusCode').should('eq', 200)
cy.wait('@me')
cy.location('pathname').should('eq', '/dashboard')

For token refresh races, assert the product retries the original business request after refresh when that is the contract. That scenario is advanced and often belongs in a focused test with stubs for 401 then success, rather than in every feature spec.

Never log real access tokens from interception objects in CI output. Assert presence of an Authorization header shape when needed without printing secrets.

Feature Flags, A/B Traffic, and Optional Requests

Feature flags can change which APIs fire. A test that always waits for @recommendations will fail when the flag is off. Options:

  1. Force the flag state in test setup so the request is guaranteed.
  2. Make the wait conditional only when the flag is on.
  3. Do not wait on optional enhancement calls for core assertions.
cy.intercept('GET', '/api/flags', {
  body: { recommendations: true },
}).as('flags')
cy.intercept('GET', '/api/recommendations').as('recommendations')

cy.visit('/home')
cy.wait('@flags')
cy.wait('@recommendations')
cy.get('[data-cy=recommendations]').should('be.visible')

Deterministic flags beat environment lottery. Document required flag states next to the spec.

Performance Awareness Without Turning Cypress Into a Load Tool

Intercept waits can reveal slow calls, but Cypress is not your load testing platform. If a wait needs a 120 second timeout to pass regularly, open a performance investigation rather than normalizing extreme timeouts across the suite.

Track a few critical aliases for unusual latency in staging if your team supports performance budgets, but keep hard load and soak scenarios in dedicated tools. A practical companion path is the API performance testing tutorial.

In Cypress, focus on functional correctness under normal latency. Use controlled delay stubs to verify loading skeletons and disabled buttons, not to simulate thousands of users.

Summary Pattern Card

When a teammate asks cypress how to wait for an API response, share this card:

  1. Intercept + alias first.
  2. Act second.
  3. Wait third.
  4. Assert response fields that matter.
  5. Assert UI outcomes users see.
  6. Stub only with intent.
  7. Delete millisecond sleeps.

If every new spec follows that card, flake rates drop and failures become explainable. That is the operational win behind the API, not only the syntax of cy.wait('@alias').

Teaching the Pattern Across a Team

Syntax is easy. Consistency is hard. Add lint guidance or PR templates that reject raw cy.wait(number) except in rare documented cases. Show one golden example per app domain (auth, search, checkout) so engineers copy a known good intercept. Over time, the suite becomes a catalog of network contracts as much as a catalog of clicks, which is exactly what you want when APIs evolve weekly.

Interview Questions and Answers

How do you synchronize Cypress tests with backend calls?

I intercept the endpoint before the action, alias it, wait on the alias, then assert UI and critical response fields. That ties the test to the real async boundary.

How do you avoid flaky waits in Cypress?

I ban fixed sleeps, use precise intercept matchers, wait only for required calls, and separate optimistic UI checks from persistence checks after confirmation responses.

How would you test a failing save that uses optimistic UI?

I stub or force a non-success response, trigger save, wait on the alias, and assert the error UI and rollback behavior rather than only the optimistic temporary state.

How do you intercept GraphQL in Cypress?

I intercept the GraphQL HTTP endpoint and assign aliases based on operationName in the request body, then wait on the specific operation the test cares about.

When is stubbing harmful?

When every critical path is stubbed, the suite can pass while the real contract breaks. I keep a slice of true integration coverage for core journeys.

How do you debug an alias that never matches?

I compare the Command Log request to the matcher, confirm intercept order, check auth redirects, and narrow or relax the matcher until it matches exactly one intended call.

What is the difference between a network wait and a DOM retry?

DOM retries assert that the UI eventually reaches a state. Network waits assert that a specific request finished. I often use both: wait for the API, then assert the DOM.

Frequently Asked Questions

How do I wait for an API call to finish in Cypress?

Declare `cy.intercept()` for the route, alias it with `.as('name')`, trigger the action, then call `cy.wait('@name')` before UI assertions that depend on the response.

Should I use cy.wait(5000) for APIs?

No. Fixed millisecond waits are flaky and slow. Wait on an intercept alias or another deterministic signal tied to the event you care about.

How do I assert the API status code after a click?

After `cy.wait('@alias')`, use `.its('response.statusCode').should('eq', 200)` or a `.then` callback that inspects the interception object.

Can I wait for multiple API responses?

Yes. Alias each route and call `cy.wait(['@one', '@two'])`, or wait sequentially when order matters for the user flow.

How do I stub an API response in Cypress?

Pass a static response or fixture to `cy.intercept`, alias it, visit or click, then wait on the alias and assert the UI rendered from the stubbed data.

Why did cy.wait('@alias') time out?

Common causes include registering the intercept too late, a mismatched method or path, the app never firing the request, or an over-strict query matcher.

Does cy.request trigger cy.intercept?

No. `cy.request` bypasses intercept routes. Use intercepts for browser-driven app traffic and `cy.request` for setup or direct API checks.

Related Guides