Deterministic Network Testing with Cypress cy.intercept()
Replace flaky waits with deterministic network control using Cypress cy.intercept(): stub responses, synchronize with aliases, and modify requests to cover error and slow-network states.
13 Jul 2026, 12:33 UTC

The Flaky Test Problem You're Actually Solving
Frontend tests often fail intermittently because they depend on network timing, third-party API availability, or backend states you cannot control. Teams patch the symptom with arbitrary cy.wait(5000) calls, which introduce new flakiness when responses arrive faster or slower than expected. Cypress's cy.intercept(), introduced in v6.0 as the replacement for cy.route(), moves network control into the test itself, letting you stub, spy on, and modify requests at the browser network layer without touching backend code.
How Interception Works at the Browser Layer
cy.intercept() registers a handler that sits between your application and the network. It matches requests by URL string or glob, HTTP method, a routeMatcher object, or a predicate function. Because it operates at the browser level, it catches fetch and XMLHttpRequest calls from your page — including calls to cross-origin endpoints, since the interception happens before the request leaves the browser. One known boundary: requests issued from inside a service worker are not intercepted, because they do not pass through the page's network layer in the same way.
You can return static fixtures, dynamic callbacks, or delayed responses. The callback receives the request object, so you can assert on headers, query parameters, or body before deciding what to reply.
Matching Order and Test Isolation
When multiple interceptors could match a request, Cypress evaluates them in reverse registration order — the most recently registered matching interceptor handles the request. So register broad matchers like **/api/** first and specific ones like /api/user/123 after, or the broad one will win. Interceptors and aliases are scoped to a single test: Cypress resets routes between tests, so an interception registered in one it() does not persist into the next. If every test in a suite needs the same stubs, register them in a beforeEach hook so each test gets a fresh, identical setup.
Synchronization Without Guessing
Alias an interception with .as('name') and wait for it via cy.wait('@name'). This replaces arbitrary millisecond waits with a deterministic synchronization point: the test pauses until the matched request fires, then resumes with access to the request and response objects for assertions.
Worked Example: Stubbing a User Profile Endpoint
Assume a component that fetches /api/user/123 on mount. Create a fixture at cypress/fixtures/user.json:
{
"id": 123,
"name": "Ada Lovelace",
"role": "engineer"
}
In your spec:
// cypress/e2e/user-profile.cy.js
describe('UserProfile', () => {
beforeEach(() => {
// Register interception before the request fires
cy.intercept('GET', '/api/user/123', { fixture: 'user.json' }).as('getUser')
})
it('renders user data from stubbed response', () => {
cy.visit('/user/123')
// Test pauses here until the request matches
cy.wait('@getUser').then((interception) => {
expect(interception.request.url).to.include('/api/user/123')
expect(interception.response.body).to.deep.equal({
id: 123,
name: 'Ada Lovelace',
role: 'engineer'
})
})
cy.contains('Ada Lovelace').should('be.visible')
cy.contains('engineer').should('be.visible')
})
})
Where to run: A terminal in a project with Cypress installed (npm i -D cypress) and a local dev server serving the page under test. Permissions: Write access to the project directory. Placeholders: Adjust the URL, fixture shape, and visited route to your application. Expected check: Run npx cypress run (headless) or npx cypress open (interactive). In open mode, intercepted requests in the command log show a stubbed indicator, and clicking the cy.wait entry reveals the request and response payloads. Risk: If the app uses a different URL or method than the matcher, the interception never fires and the real network call proceeds — the test then flakes or fails depending on backend availability.
Modifying Requests for Edge Cases
The interception callback receives req with methods to control the outcome:
req.reply({ statusCode: 500, body: 'Server Error' })— test error boundaries without a failing backend.req.reply({ delay: 2000, fixture: 'user.json' })— simulate slow networks for loading-state coverage.req.continue((res) => { ... })— pass the request to the real server, then inspect or modify the real response.req.destroy()— simulate a hard network failure.
Header injection (for example, adding an authorization token) is possible by modifying req.headers before calling req.continue(), which lets you hit protected endpoints without test-specific backend routes.
Trade-offs and Limitations
| Limitation | Impact | Mitigation |
|---|---|---|
| Stubbed responses bypass the real network stack | CORS preflight, TLS negotiation, and real server behavior go untested | Keep a small suite of true integration tests against a staging environment |
| Service worker requests not intercepted | Apps relying on service-worker caching can't be fully stubbed this way | Test those flows against a real backend or disable the service worker in test |
| Binary fixtures | String fixtures can corrupt binary payloads like PDFs | Read the file with cy.readFile(path, null) and reply with the ArrayBuffer |
| Matcher order | A broad glob registered later shadows specific interceptors | Register broad matchers first, specific ones last |
One caveat worth flagging: interception behavior has evolved across major Cypress versions (notably around session handling and cross-origin navigation with cy.origin()). The patterns above reflect long-standing behavior, but verify against the changelog for your installed version before relying on edge cases — the examples here were not executed against a specific release.
Verify It Works in Your Context
- Create a scratch project:
npm init -y && npm i -D cypress. - Add a spec with
cy.intercept('/api/user', { fixture: 'user.json' }).as('getUser')pointed at a page you control that makes that request. - Run
npx cypress openand confirm the interception fires and thecy.wait('@getUser')entry shows the stubbed payload. - Deliberately break the matcher (wrong path) and confirm the test behavior changes — this proves the interception, not the backend, is driving the result.
Next Steps
Replace one cy.wait(ms) in your suite with an aliased cy.intercept() plus cy.wait('@alias'), and watch CI flakiness over the following runs. Then add error-state coverage with req.reply({ statusCode: 500 }) — no backend changes required. The same interception API also works in Cypress component testing, so you can stub API contracts while mounting components in isolation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.