Cypress Flaky Tests: Root Causes, Detection, and Fixes (Complete Guide)
If you have ever watched a Cypress test pass five times in a row and then inexplicably fail on the sixth run with no code changes, you are not alone. Flaky Cypress tests are one of the most common frustrations for front-end and QA teams adopting end-to-end testing. They erode trust in your test suite, slow down CI/CD pipelines, and waste hours of engineering time chasing phantom failures.
This guide covers everything you need to know about Cypress flaky tests: why they happen, how to detect them systematically, and battle-tested fixes for every major category of flakiness. Whether you are dealing with timing issues in cy.intercept, race conditions in the command queue, or test isolation failures, you will find actionable solutions here.
Why Cypress Tests Become Flaky
Before diving into fixes, it is worth understanding what makes Cypress tests uniquely susceptible to flakiness -- and also uniquely equipped to fight it.
Cypress runs inside the browser alongside your application. This gives it powerful capabilities like automatic waiting, network interception, and direct DOM access. But it also means your tests are subject to every timing nuance of browser rendering, JavaScript execution, and network behavior.
The Three Pillars of Cypress Flakiness
Most flaky Cypress tests fall into one of three categories:
Understanding which category your flaky test belongs to is the first step toward fixing it.
Root Cause #1: The Cypress Command Queue and Timing Issues
Cypress commands do not execute immediately. They are enqueued and run asynchronously in a deterministic order. This is one of Cypress's greatest strengths, but misunderstanding how the command queue works is the number one source of flaky tests.
How the Command Queue Works
When you write Cypress code like this:
cy.get('.submit-button').click();
cy.get('.success-message').should('be.visible');
These commands are not executed line by line. Instead, Cypress adds them to a queue and processes them sequentially. Each command waits for the previous one to complete before executing. This is why you do not need explicit await statements in Cypress.
The Mixing Problem: Cypress Commands and Regular JavaScript
Problems arise when you mix Cypress commands with synchronous JavaScript:
// PROBLEMATIC: Mixing sync JS with Cypress commands
let userName;
cy.get('.user-name').then(($el) => {
userName = $el.text();
});
// This runs BEFORE the .then() callback!
console.log(userName); // undefined
cy.get('.greeting').should('contain', userName); // flaky!
The fix is to keep everything inside the Cypress command chain:
// CORRECT: Stay within the command chain
cy.get('.user-name').then(($el) => {
const userName = $el.text();
cy.get('.greeting').should('contain', userName);
});
Conditional Testing Pitfalls
Conditional testing is another area where the command queue causes flakiness:
// FLAKY: The condition might evaluate at the wrong time
cy.get('body').then(($body) => {
if ($body.find('.modal').length > 0) {
cy.get('.modal .close').click();
}
});
cy.get('.main-content').should('be.visible');
The problem is that the modal might appear after the $body.find() check but before the next assertion. A more robust approach uses explicit waits or assertions:
// BETTER: Use an assertion-based approach
cy.get('.main-content', { timeout: 10000 }).should('be.visible');
Race Conditions with cy.then()
The cy.then() command is essential but can introduce race conditions when used carelessly:
// FLAKY: Multiple .then() blocks that depend on shared state
let itemCount;
cy.get('.items').then(($items) => {
itemCount = $items.length;
});
cy.get('.add-item').click();
cy.get('.items').then(($items) => {
// itemCount might not be set yet in some edge cases
expect($items.length).to.equal(itemCount + 1);
});
The reliable pattern nests dependent operations:
// RELIABLE: Nested dependencies
cy.get('.items').its('length').then((initialCount) => {
cy.get('.add-item').click();
cy.get('.items').should('have.length', initialCount + 1);
});
Root Cause #2: Network Interception Timing with cy.intercept
Network-related flakiness is extremely common in Cypress tests. The cy.intercept() API is powerful, but timing issues with network requests are a leading cause of flaky tests.
The Setup-Before-Action Rule
The most fundamental rule with cy.intercept() is that you must set up the intercept before the action that triggers the network request:
// FLAKY: Intercept set up too late
cy.get('.load-data').click();
cy.intercept('GET', '/api/data').as('getData');
cy.wait('@getData'); // Might miss the request entirely!
// CORRECT: Intercept before the trigger
cy.intercept('GET', '/api/data').as('getData');
cy.get('.load-data').click();
cy.wait('@getData');
Handling Multiple Requests to the Same Endpoint
Applications often make multiple requests to the same endpoint. This causes flakiness when your test waits for the wrong one:
// FLAKY: Might catch the wrong request
cy.intercept('GET', '/api/users*').as('getUsers');
cy.visit('/dashboard');
cy.wait('@getUsers'); // Catches the initial load
cy.get('.refresh').click();
cy.wait('@getUsers'); // Might still be waiting on the first request!
Use numbered aliases or more specific matching:
// RELIABLE: Use specific request matching
cy.intercept('GET', '/api/users*').as('getUsers');
cy.visit('/dashboard');
cy.wait('@getUsers');
// Set up a new intercept for the refresh
cy.intercept('GET', '/api/users*').as('getUsersRefresh');
cy.get('.refresh').click();
cy.wait('@getUsersRefresh');
Stubbing vs. Letting Requests Through
One of the most impactful decisions for test reliability is whether to stub network requests or let them hit a real server:
// STUBBED: Deterministic and fast
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: {
products: [
{ id: 1, name: 'Widget', price: 9.99 },
{ id: 2, name: 'Gadget', price: 19.99 },
],
},
}).as('getProducts');
// REAL: Subject to server timing, data changes, network issues
cy.intercept('GET', '/api/products').as('getProducts');
For tests that verify UI behavior, stub the network. For integration tests that verify API contracts, let requests through but add generous timeouts and proper error handling.
Handling Slow or Delayed Responses
Sometimes flakiness comes from the server responding faster or slower than expected:
// Simulate realistic network delay to catch timing bugs
cy.intercept('POST', '/api/submit', (req) => {
req.reply({
delay: 500,
statusCode: 200,
body: { success: true },
});
}).as('submitForm');
cy.get('.submit').click();
// Now test the loading state
cy.get('.loading-spinner').should('be.visible');
cy.wait('@submitForm');
cy.get('.loading-spinner').should('not.exist');
cy.get('.success-message').should('be.visible');
Root Cause #3: Retry-ability and Assertions
Cypress has a built-in retry mechanism for assertions, but not all commands are retryable. Misunderstanding which commands retry and which do not is a major source of flakiness.
Commands That Retry vs. Commands That Do Not
Retryable commands include cy.get(), cy.find(), cy.contains(), and most query commands. Non-retryable commands include cy.click(), cy.type(), cy.then(), and most action commands.
This distinction matters enormously:
// FLAKY: .then() does not retry
cy.get('.items').then(($items) => {
expect($items).to.have.length(5); // Does not retry if items are still loading
});
// RELIABLE: .should() retries until passing or timeout
cy.get('.items').should('have.length', 5); // Retries automatically
Chaining Assertions for Retry-ability
You can chain multiple assertions, and Cypress will retry the entire chain:
// All assertions retry together
cy.get('.user-card')
.should('be.visible')
.and('contain', 'John Doe')
.and('have.class', 'active');
Custom Retry Logic with should() Callbacks
For complex assertions, use the callback form of should():
// Custom retry logic
cy.get('.data-table').should(($table) => {
const rows = $table.find('tr');
expect(rows.length).to.be.greaterThan(0);
const firstRow = rows.first();
expect(firstRow.find('td').first().text().trim()).to.not.be.empty;
});
This entire callback retries until all expectations pass or the timeout expires.
Adjusting Timeouts Strategically
Rather than using a global timeout increase (which slows down your entire suite), adjust timeouts for specific commands:
// Targeted timeout for slow operations
cy.get('.analytics-dashboard', { timeout: 15000 }).should('be.visible');
// Default timeout for fast operations
cy.get('.nav-menu').should('be.visible');
Root Cause #4: Test Isolation Failures
Test isolation failures are insidious because they create flakiness that only appears when tests run in a specific order. A test might pass when run alone but fail when run as part of the full suite.
Shared State Through the Application
The most common isolation failure is shared application state:
// Test A: Creates data
it('should create a new item', () => {
cy.get('.add-item').click();
cy.get('.item-name').type('Test Item');
cy.get('.save').click();
cy.get('.items-list').should('contain', 'Test Item');
});
// Test B: Assumes clean state - FLAKY if Test A runs first
it('should show empty state when no items exist', () => {
cy.visit('/items');
cy.get('.empty-state').should('be.visible'); // Fails because Test A created an item!
});
Fixing Isolation with beforeEach
Use beforeEach hooks to reset state:
beforeEach(() => {
// Reset database state via API
cy.request('POST', '/api/test/reset');
// Clear browser state
cy.clearCookies();
cy.clearLocalStorage();
// Visit the page fresh
cy.visit('/items');
});
Cypress Session API for Efficient Isolation
Cypress's cy.session() API lets you cache and restore login state efficiently while maintaining isolation:
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.visit('/login');
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('.login-button').click();
cy.url().should('include', '/dashboard');
});
});
beforeEach(() => {
cy.login('testuser', 'password123');
});
Local Storage and Cookie Leakage
Even with cy.clearCookies(), some state can persist. Be thorough:
beforeEach(() => {
cy.clearCookies();
cy.clearLocalStorage();
// Clear sessionStorage too
cy.window().then((win) => {
win.sessionStorage.clear();
});
// Clear IndexedDB if your app uses it
cy.window().then((win) => {
win.indexedDB.databases().then((databases) => {
databases.forEach((db) => {
win.indexedDB.deleteDatabase(db.name);
});
});
});
});
Root Cause #5: DOM and Rendering Timing
Even with Cypress's built-in waiting, DOM timing issues can cause flakiness, especially with modern front-end frameworks that use virtual DOM diffing and asynchronous rendering.
Animations and Transitions
CSS animations and transitions are a frequent source of flakiness:
// FLAKY: Clicking during an animation
cy.get('.dropdown-trigger').click();
cy.get('.dropdown-menu .item').first().click(); // Might fail if menu is animating
// RELIABLE: Wait for animation to complete
cy.get('.dropdown-trigger').click();
cy.get('.dropdown-menu')
.should('be.visible')
.and('not.have.class', 'animating');
cy.get('.dropdown-menu .item').first().click();
You can also disable animations globally in your test setup:
// cypress/support/e2e.js;beforeEach(() => {
cy.document().then((doc) => {
const style = doc.createElement('style');
style.innerHTML =
, ::before, *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
doc.head.appendChild(style);
});
});
Detached DOM Elements
When frameworks re-render components, DOM elements get replaced. If Cypress holds a reference to an old element, commands will fail:
// FLAKY: Element might get detached between commands
cy.get('.user-list li').first().as('firstUser');
cy.get('.refresh').click(); // Triggers re-render
cy.get('@firstUser').click(); // Element is detached!
// RELIABLE: Re-query after actions that trigger re-renders
cy.get('.refresh').click();
cy.get('.user-list li').first().click();
React, Vue, and Angular Specific Issues
Each framework has its own rendering quirks:
React: State updates are batched and asynchronous. After triggering a state change, wait for the resulting DOM update:cy.get('.increment').click();
// React batches updates - wait for the new value
cy.get('.counter').should('have.text', '1');
Vue: Vue uses nextTick for DOM updates. Similar waiting patterns apply:
cy.get('.toggle').click();
cy.get('.content').should('be.visible'); // Vue will update on next tick
Angular: Zone.js can interfere with Cypress's waiting. Use cy.wait() sparingly for Angular-specific stabilization issues, or configure Cypress to wait for Angular zones to stabilize.
Root Cause #6: Viewport and Responsive Design Flakiness
Tests that pass on one viewport size but fail on another are a subtle form of flakiness:
// Set viewport explicitly at the start of each test
beforeEach(() => {
cy.viewport(1280, 720);
});
// Or test multiple viewports explicitly
const viewports = ['iphone-6', 'ipad-2', [1280, 720]];
viewports.forEach((viewport) => {
it(should display navigation correctly on ${viewport}, () => {
if (Array.isArray(viewport)) {
cy.viewport(viewport[0], viewport[1]);
} else {
cy.viewport(viewport);
}
cy.visit('/');
cy.get('.nav').should('be.visible');
});
});
Detecting Flaky Cypress Tests Systematically
Finding flaky tests before they become a problem requires a systematic approach.
Cypress's Built-in Retry Mechanism
Cypress has a built-in test retry feature that you can configure in cypress.config.js:
module.exports = defineConfig({
retries: {
runMode: 2, // Retries in CI
openMode: 0, // No retries in interactive mode
},
});
While retries mask flakiness in CI, they also help identify flaky tests. Any test that needs a retry to pass should be investigated.
Running Tests Multiple Times
The simplest detection method is to run your tests multiple times:
# Run the same spec 10 times
for i in {1..10}; do
npx cypress run --spec "cypress/e2e/checkout.cy.js"
done
Using DeFlaky for Automated Detection
Manual detection is tedious and error-prone. DeFlaky automates the entire process by running your test suite multiple times, tracking pass/fail patterns, and calculating a FlakeScore for each test:
# Install DeFlaky
npm install -g deflaky
Analyze your Cypress tests for flakiness
deflaky analyze --framework cypress --runs 10
View the dashboard for detailed results
deflaky dashboard
DeFlaky integrates directly with Cypress and can identify flaky tests that only fail under specific conditions -- like when run in parallel, after a particular test, or on specific browser versions. Its dashboard shows trends over time, helping you prioritize which flaky tests to fix first based on their impact on your pipeline.
Monitoring CI Failure Patterns
Track your CI failures over time. A test that fails once every 20 runs is flaky. Key metrics to watch:
Building Custom Commands for Reliability
Custom Cypress commands can encapsulate reliability patterns so your team does not have to remember them every time:
A Reliable Click Command
Cypress.Commands.add('safeClick', (selector, options = {}) => {
const { timeout = 10000, force = false } = options;
cy.get(selector, { timeout })
.should('be.visible')
.should('not.be.disabled')
.then(($el) => {
// Ensure element is not covered by another element
const rect = $el[0].getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const topElement = Cypress.$(document.elementFromPoint(centerX, centerY));
if (topElement.is($el) || $el.find(topElement).length > 0 || topElement.closest(selector).length > 0) {
cy.wrap($el).click({ force });
} else {
// Scroll into view and retry
cy.wrap($el).scrollIntoView().click({ force });
}
});
});
A Reliable Form Fill Command
Cypress.Commands.add('fillForm', (formData) => {
Object.entries(formData).forEach(([selector, value]) => {
cy.get(selector)
.should('be.visible')
.clear()
.type(value, { delay: 50 })
.should('have.value', value);
});
});
// Usage
cy.fillForm({
'#first-name': 'John',
'#last-name': 'Doe',
'#email': 'john@example.com',
});
A Reliable Wait-for-API Command
Cypress.Commands.add('waitForApi', (method, url, alias, options = {}) => {
const { timeout = 30000, statusCode = 200 } = options;
cy.intercept(method, url).as(alias);
return cy.wait(@${alias}, { timeout }).then((interception) => {
if (statusCode) {
expect(interception.response.statusCode).to.equal(statusCode);
}
return interception;
});
});
Advanced Fix Strategies
Strategy 1: Deterministic Test Data
Never rely on shared or production-like data for tests. Create fresh data for each test:
beforeEach(() => {
// Seed the database with known state
cy.task('db:seed', {
users: [
{ id: 1, name: 'Test User', email: 'test@example.com' },
],
products: [
{ id: 1, name: 'Widget', price: 9.99, stock: 100 },
],
});
});
Strategy 2: Clock Control for Time-Dependent Tests
Tests that depend on the current time are inherently flaky:
// FLAKY: Depends on current time
it('should show morning greeting before noon', () => {
cy.visit('/dashboard');
cy.get('.greeting').should('contain', 'Good morning'); // Only works before noon!
});
// RELIABLE: Control the clock
it('should show morning greeting before noon', () => {
const morning = new Date(2026, 3, 7, 9, 0, 0); // April 7, 2026, 9:00 AM
cy.clock(morning.getTime());
cy.visit('/dashboard');
cy.get('.greeting').should('contain', 'Good morning');
});
Strategy 3: Parallelization Without Interference
When running Cypress tests in parallel, ensure tests do not interfere with each other:
// Use unique identifiers per test worker
const workerId = Cypress.env('WORKER_ID') || '0';
it('should create and verify an order', () => {
const uniqueEmail = test-${workerId}-${Date.now()}@example.com;
cy.get('#email').type(uniqueEmail);
cy.get('#submit').click();
cy.get('.confirmation').should('contain', uniqueEmail);
});
Strategy 4: Handling Third-Party Scripts
Third-party scripts (analytics, chat widgets, ads) can interfere with tests:
// Block third-party scripts in tests
beforeEach(() => {
cy.intercept('GET', 'https://www.google-analytics.com/**', { statusCode: 200, body: '' });
cy.intercept('GET', 'https://cdn.intercom.io/**', { statusCode: 200, body: '' });
cy.intercept('GET', '/hotjar.com/', { statusCode: 200, body: '' });
});
Strategy 5: Visual Stability Assertions
Instead of relying solely on text or attribute assertions, verify visual stability:
// Wait for the page to be visually stable before interacting
Cypress.Commands.add('waitForStable', (selector, options = {}) => {
const { timeout = 10000, interval = 200 } = options;
let previousHtml = '';
let stableCount = 0;
const requiredStableChecks = 3;
const checkStability = () => {
return cy.get(selector, { timeout }).then(($el) => {
const currentHtml = $el.html();
if (currentHtml === previousHtml) {
stableCount++;
} else {
stableCount = 0;
previousHtml = currentHtml;
}
if (stableCount < requiredStableChecks) {
cy.wait(interval);
return checkStability();
}
});
};
return checkStability();
});
Cypress Configuration for Maximum Reliability
Here is a production-tested cypress.config.js that minimizes flakiness:
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
defaultCommandTimeout: 10000,
requestTimeout: 15000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
retries: {
runMode: 2,
openMode: 0,
},
video: true,
screenshotOnRunFailure: true,
// Improve test isolation
testIsolation: true,
// Disable Chrome Web Security for cross-origin testing
chromeWebSecurity: false,
// Experimental features for stability
experimentalMemoryManagement: true,
setupNodeEvents(on, config) {
// Increase memory for large test suites
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium') {
launchOptions.args.push('--disable-gpu');
launchOptions.args.push('--disable-dev-shm-usage');
launchOptions.args.push('--no-sandbox');
launchOptions.args.push('--disable-extensions');
}
return launchOptions;
});
return config;
},
},
});
A Flaky Test Debugging Checklist
When you encounter a flaky Cypress test, work through this checklist:
cy.intercept() to log all network requests and check for unexpected failures or slow responses.cy.wait(number), hardcoded timeouts, or assertions that assume immediate updates.Integrating Flaky Test Detection into Your Workflow
The best teams do not just fix flaky tests reactively -- they prevent them proactively.
Pre-Merge Detection
Run each PR's tests multiple times before merging to catch flakiness early:
# GitHub Actions example
name: Cypress Flaky Test Detection
on: pull_request
jobs:
flaky-detection:
runs-on: ubuntu-latest
strategy:
matrix:
run: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- uses: cypress-io/github-action@v6
with:
command: npx cypress run
Continuous Monitoring with DeFlaky
DeFlaky can run as part of your CI pipeline and track flakiness trends over time. It automatically quarantines tests that exceed a configurable flake threshold, preventing them from blocking deployments while alerting the team to fix them:
# DeFlaky CI integration
- name: Run DeFlaky Analysis
run: |
deflaky ci --framework cypress \
--threshold 0.05 \
--quarantine-on-flake \
--notify slack
The DeFlaky dashboard provides historical FlakeScore trends, letting you see whether your test suite is getting more or less reliable over time. Teams that track this metric consistently reduce their flake rate by 60-80% within three months.
Team Practices
Beyond tooling, adopt these team practices:
Common Cypress Anti-Patterns That Cause Flakiness
Anti-Pattern 1: Arbitrary cy.wait()
// BAD: Arbitrary wait
cy.get('.button').click();
cy.wait(3000);
cy.get('.result').should('exist');
// GOOD: Wait for a specific condition
cy.get('.button').click();
cy.get('.result', { timeout: 10000 }).should('be.visible');
Anti-Pattern 2: Testing Through the UI for Setup
// BAD: UI-based setup is slow and flaky
it('should edit a product', () => {
cy.visit('/admin/login');
cy.get('#username').type('admin');
cy.get('#password').type('admin123');
cy.get('.login').click();
cy.visit('/admin/products');
cy.get('.add-product').click();
// ... 20 more steps just to create a product to edit
// NOW we test the actual edit functionality
cy.get('.edit-product').first().click();
});
// GOOD: API-based setup is fast and reliable
it('should edit a product', () => {
cy.request('POST', '/api/products', { name: 'Test Product', price: 10 });
cy.login('admin'); // Custom command using cy.session()
cy.visit('/admin/products');
cy.get('.edit-product').first().click();
});
Anti-Pattern 3: Over-Specific Selectors
// BAD: Brittle selector
cy.get('div.container > div:nth-child(2) > ul > li:first-child > a');
// GOOD: Data attribute selector
cy.get('[data-testid="first-item-link"]');
Anti-Pattern 4: Not Waiting for Navigation
// BAD: Assuming instant navigation
cy.get('.nav-link').click();
cy.get('.page-content').should('exist'); // Might check before navigation starts
// GOOD: Wait for URL change
cy.get('.nav-link').click();
cy.url().should('include', '/target-page');
cy.get('.page-content').should('be.visible');
Anti-Pattern 5: Ignoring Uncaught Exceptions
Uncaught exceptions in your application will fail Cypress tests. Handle them explicitly:
// In cypress/support/e2e.js
Cypress.on('uncaught:exception', (err, runnable) => {
// Known third-party errors that don't affect our tests
if (err.message.includes('ResizeObserver loop')) {
return false; // Prevent test failure
}
// Let other errors fail the test
return true;
});
Measuring Your Progress
Track these metrics to measure your flaky test improvement:
| Metric | Target | How to Measure |
|--------|--------|----------------|
| Flake Rate | < 1% | Failed runs / Total runs per test |
| Mean Time to Detect | < 1 day | Time from introduction to detection |
| Mean Time to Fix | < 2 days | Time from detection to fix |
| Retry Rate | < 5% | Tests requiring retry / Total tests |
| Suite Reliability | > 99% | Clean runs / Total suite runs |
DeFlaky tracks all of these metrics automatically and provides trend analysis, letting you demonstrate concrete improvement to stakeholders.
Conclusion
Flaky Cypress tests are not inevitable. By understanding the command queue, mastering cy.intercept() timing, leveraging retry-ability, ensuring test isolation, and using tools like DeFlaky for automated detection and monitoring, you can build a Cypress test suite that your team actually trusts.
The key is to treat test reliability as a first-class engineering concern, not an afterthought. Every flaky test that goes unfixed erodes trust in your entire test suite, slows down your CI pipeline, and wastes engineering time. Start with the quick wins -- fixing arbitrary waits, adding proper assertions, and stubbing network requests -- then build toward a comprehensive reliability strategy that includes automated detection, monitoring, and team practices.
Your future self, staring at a green CI pipeline at 5 PM on a Friday, will thank you.