Testing & QA

How to Fix Flaky Tests: Strategies for Reliable CI/CD

Flaky tests erode developer trust and slow down deployments. This deep dive by Krapton Engineering unpacks the core reasons behind unreliable tests—from async timing to shared state—and provides actionable strategies to diagnose, prevent, and fix flaky tests, ensuring your CI/CD pipeline delivers consistent, trustworthy results.

Krapton Engineering
Reviewed by a senior engineer10 min read
Share
How to Fix Flaky Tests: Strategies for Reliable CI/CD

In the relentless pace of modern software development, a robust Continuous Integration/Continuous Deployment (CI/CD) pipeline is non-negotiable. Yet, for many engineering teams, the promise of rapid, confident deployments is undermined by one pervasive enemy: flaky tests. These unreliable gatekeepers, passing one moment and failing the next without any code change, are a top reason teams stop trusting their CI, leading to ignored alerts, slower review cycles, and ultimately, a hesitant release cadence.

TL;DR: Flaky tests stem from non-deterministic factors like async timing, shared state, or environment inconsistencies. Fixing them requires systematic diagnosis, robust wait strategies, rigorous test isolation, and smart dependency management to restore trust and accelerate your CI/CD pipeline.

Key takeaways

Close-up of test tubes filled with red liquid in a laboratory setting.
Photo by Ivan S on Pexels
  • Flaky tests are non-deterministic failures that erode trust in CI/CD and slow development.
  • Root causes include async operations, shared mutable state, and environmental inconsistencies.
  • Effective solutions involve robust explicit waits, strict test isolation, and strategic mocking of external dependencies.
  • Prioritizing integration tests and deterministic test data builds long-term stability.
  • A reliable testing strategy leads to faster deployments, quicker PR reviews, and higher developer confidence.

The Hidden Cost of Flaky Tests in CI/CD

Blood samples in test tubes on a light background, ideal for medical and laboratory themes.
Photo by www.kaboompics.com on Pexels

The impact of flaky tests extends far beyond individual test failures. They introduce noise into the development process, making it difficult to distinguish genuine regressions from false alarms. This constant uncertainty forces developers to re-run builds, manually verify changes, or even disable tests entirely, all of which negate the core benefits of automated testing.

From a business perspective, the costs are substantial. Wasted CI/CD resources, delayed feature releases, and reduced developer morale directly impact productivity and market responsiveness. In a recent client engagement, we observed a team spending nearly 15% of their daily engineering hours simply debugging or re-running builds due to persistent flakiness across their E2E suite. This wasn't just a technical problem; it was a significant drag on their ability to innovate.

Common Root Causes of Flaky Tests and How to Diagnose Them

Flakiness is rarely random; it almost always points to a non-deterministic element in your test environment or logic. Identifying these root causes is the first step towards a stable test suite.

Asynchronous Operations and Timing Issues

Many web and mobile applications are inherently asynchronous, relying on network requests, animations, or delayed processing. When tests don't correctly wait for these operations to complete, they can race ahead and assert on an outdated or incomplete UI state.

Consider a Playwright E2E test that clicks a button and immediately checks for a UI change that depends on a backend API call:

// ❌ Flaky: No explicit wait for async operation to complete
await page.locator('#submit-button').click();
await expect(page.locator('.success-message')).toBeVisible(); // Might fail if API is slow

This test might pass on a fast local machine but fail intermittently in CI due to network latency or server load. Diagnosing these often involves adding verbose logging around assertions, inspecting network requests in browser developer tools, or using CI-specific retry mechanisms to confirm intermittent failures.

Shared State and Test Isolation Breakdowns

Tests must be isolated and independent. When tests share mutable state—be it in a database, local storage, global variables, or even a shared file system—the order in which they run can impact their outcome. A test might inadvertently leave data in a state that causes a subsequent test to fail.

Imagine two tests interacting with the same user session data in a browser's local storage:

// ❌ Flaky: Shared local storage state between tests
test('should log in user A', async ({ page }) => {
  await page.goto('/login');
  // ... login user A, sets local storage ...
});

test('should log in user B', async ({ page }) => {
  // If user A's session is not cleared, this might fail or behave unexpectedly
  await page.goto('/login');
  // ... login user B ...
});

Diagnosing shared state issues requires careful auditing of global resources and ensuring proper teardown and setup routines. Tools like a debugger that can pause execution between tests can reveal lingering state.

Environmental Inconsistencies and External Dependencies

The environment where tests run can introduce non-determinism. Differences in operating systems, browser versions, network conditions, or the availability and responsiveness of external APIs can all cause flakiness. For instance, a test relying on a third-party payment gateway might fail if the gateway experiences a momentary outage or rate limits the test environment.

On a production rollout we shipped, the failure mode was a payment processing E2E test that would randomly fail once every 10-15 runs in CI. The root cause wasn't our code, but a subtle difference in network routing between our CI provider's data centers and the payment gateway's sandbox environment, causing occasional, small spikes in latency that exceeded our test's implicit wait times. This highlights how external factors, often beyond direct control, can be significant sources of flakiness.

Proven Strategies to Fix Flaky Tests and Enhance Stability

Addressing flakiness requires a proactive, systematic approach focusing on determinism and isolation.

Implementing Robust Wait Strategies

For asynchronous operations, explicit waits are paramount. Instead of arbitrary setTimeout calls, use testing framework-specific waits that poll for conditions to be met. Playwright, for example, offers powerful waiting mechanisms:

// ✅ Robust: Explicitly waits for the success message to be visible
await page.locator('#submit-button').click();
await expect(page.locator('.success-message')).toBeVisible({ timeout: 10000 }); // Waits up to 10s

// Or wait for network to be idle after a navigation/action
await page.goto('/dashboard');
await page.waitForLoadState('networkidle'); // Playwright's `page.waitForLoadState()` method

For complex scenarios, expect.poll() allows you to repeatedly check a condition until it passes or times out, ideal for eventual consistency patterns.

Achieving True Test Isolation

Each test should run in a pristine, predictable environment. This means:

  • Ephemeral Databases: Use tools that can spin up a fresh database instance for each test run (or even each test file) and tear it down afterward. This might involve Docker containers for Postgres 16 or in-memory databases.
  • Test Data Factories: Programmatically create only the data needed for a specific test, avoiding reliance on existing data that might change.
  • Clearing Browser State: Ensure cookies, local storage, and session storage are cleared between tests or use a fresh browser context for each test. Playwright's test.beforeEach and test.afterEach hooks are excellent for this.

Our team measured a 30% reduction in CI build times and a 90% decrease in reported flaky E2E failures on a large React application by implementing a strategy of per-test-file ephemeral database instances combined with a robust test data factory. This upfront investment in isolation paid dividends in developer confidence and release velocity.

Managing External Dependencies with Mocking and Stubs

External services (APIs, payment gateways, authentication providers) are major sources of non-determinism. For unit and integration tests, mock or stub these dependencies. For E2E tests, consider network interception to control responses.

// ✅ Robust: Mocking an API call in Playwright
await page.route('/api/users', route => {
  route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([{ id: '1', name: 'Krapton User' }]),
  });
});
await page.goto('/users');
await expect(page.locator('text=Krapton User')).toBeVisible();

This approach makes tests faster and more reliable by removing the variability of the actual external service. However, it's crucial to ensure your mocks accurately reflect the real API contracts. This is where custom API development and contract testing become invaluable.

Architecting for Testability: Beyond Reactive Flakiness Fixes

While fixing existing flaky tests is essential, a long-term strategy involves architecting your application and test suite for inherent stability.

The Evolving Test Strategy: Integration-Heavy Approach

The traditional test pyramid, with a broad base of unit tests, a mid-layer of integration tests, and a small apex of E2E tests, is flattening. Many teams are moving towards a 'test honeycomb' or 'testing trophy' where integration tests form the largest layer. These tests offer a good balance of coverage and speed, verifying interactions between components or services without the full overhead and flakiness potential of browser-driven E2E tests.

AspectTraditional Test PyramidModern Test Honeycomb/Trophy
E2E TestsBroad top layer, often brittleSmaller, critical paths only
Integration TestsMid-layer, service-levelLargest layer, component & service interaction
Unit TestsBroad base, isolated functionsSolid base, core logic
SpeedSlower overall due to E2E volumeFaster, less CI/CD bottleneck
FlakinessHigher potential due to E2E volumeLower, more deterministic
MaintenanceHigh, especially for UI changesModerate, focused on interfaces

This shift means investing in robust component-level testing for frameworks like React with React Server Components (RSC) and Next.js 15.2 App Router, and ensuring comprehensive API integration tests that cover data flow between services. This focus on lower-level integration tests reduces the surface area for flakiness, as fewer tests rely on the full, complex, and often unpredictable browser environment.

Prioritizing Deterministic Test Data

Randomly generated or production-seeded data can introduce subtle flakiness. Instead, implement a strategy for deterministic test data. This means:

  • Seeding with known values: For each test, seed your database with precisely the data needed.
  • Factories with fixed inputs: Use data factories that generate consistent, predictable data every time.
  • Versioned data: If your schema evolves, ensure your test data generators are versioned to match.

When NOT to use this approach

While crucial, excessive mocking of every dependency in E2E tests can lead to a false sense of security, where tests pass but the real system fails due to subtle integration issues. This approach is less suitable for complex, multi-service workflows where the primary goal is end-to-end user journey validation across actual integrated systems. In such cases, a small, highly stable set of E2E tests against a realistic staging environment (with robust data cleanup) is preferable, complemented by extensive contract and integration tests at lower layers. Over-optimizing for speed by mocking everything at the E2E layer can mask critical production problems.

Real-World Payoff: Deploy Confidence and Faster Development Cycles

The effort to fix flaky tests and build a more stable testing strategy yields significant returns. Teams gain true confidence in their CI/CD pipeline, knowing that a red build indicates a genuine issue, not just a random glitch. This translates directly to:

  • Faster Deployments: Fewer re-runs and manual checks mean features ship sooner.
  • Quicker Code Reviews: Developers trust the CI status, accelerating the review and merge process.
  • Higher Developer Morale: Less frustration with unreliable tools and more focus on building features.
  • Reduced Debugging Time: Real regressions are easier to spot and fix.

By investing in test stability, you empower your engineering team to move with speed and precision, delivering high-quality software consistently.

FAQ: Fixing Flaky Tests

What is a flaky test?

A flaky test is an automated test that produces different results (pass or fail) on different runs, despite no changes being made to the code or test environment. This non-deterministic behavior makes tests unreliable and undermines confidence in the CI/CD pipeline.

How do I identify the root cause of test flakiness?

Begin by reproducing the flakiness locally, often with repeated runs. Look for common patterns like inconsistent timing, shared mutable state, or external dependency variability. Use verbose logging, network inspection, and framework-specific debugging tools to pinpoint non-deterministic interactions.

Can AI help fix flaky tests?

AI-assisted tools can help by analyzing test logs to identify patterns of flakiness, suggesting optimal wait times, or even generating more robust selectors. However, human-owned assertions are critical; AI primarily assists in diagnosis and pattern identification, not in fundamentally correcting non-deterministic logic without human oversight.

What's the role of Playwright in preventing flaky tests?

Playwright excels at preventing flaky tests by offering powerful, built-in auto-waiting mechanisms and explicit wait conditions. Its ability to intercept network requests, manage browser contexts, and provide robust selectors makes it a strong choice for building stable and reliable E2E tests, reducing the common causes of flakiness.

Build Trusted Software with Krapton Engineering

At Krapton, we understand that quality software isn't just about writing code; it's about building systems that are rigorously tested and confidently deployable. Our principal engineers specialize in architecting resilient testing strategies, from unit and integration tests to advanced E2E automation with Playwright, ensuring your applications are stable, performant, and reliable from development to production. Want shipping confidence? You can book a free consultation with Krapton to build and scale your next product with a team that prioritizes quality.

About the author

Krapton Engineering builds and ships robust web, mobile, and SaaS applications for startups and enterprises globally. Our principal engineers have over a decade of hands-on experience in architecting resilient testing strategies, debugging complex CI/CD failures, and ensuring production stability for high-traffic systems.

testingflaky testse2e testingtest automationci/cdplaywrightqatest stabilitysoftware qualitydebugging
About the author

Krapton Engineering

Krapton Engineering builds and ships robust web, mobile, and SaaS applications for startups and enterprises globally. Our principal engineers have over a decade of hands-on experience in architecting resilient testing strategies, debugging complex CI/CD failures, and ensuring production stability for high-traffic systems.