Resource library

QA How-To

How to Speed up a slow test suite (2026)

Learn how to speed up a slow test suite with baselines, pyramid demotion, auth setup, parallel workers, CI sharding, flake control, and practical 2026 configs.

22 min read | 2,340 words

TL;DR

Profile where time goes, remove or demote low-value end-to-end tests, push checks down the pyramid, isolate data, parallelize carefully, and shard in CI. Never trade trust for a shorter wall-clock number.

Key Takeaways

  • Baseline wall time, install time, retries, and the slowest tests before changing concurrency.
  • The biggest wins often come from deleting or demoting low-value end-to-end cases.
  • Replace UI logins and sleeps with storage state, API seeds, and deterministic waits.
  • Parallelize only after isolation; then shard across CI when one host is not enough.
  • Keep browser matrices intentional: thin on PR, deeper on main or nightly.
  • Treat retry time as cost and protect first-attempt pass rate.
  • Install duration budgets and monthly reviews so the suite does not slowly rot again.

Knowing how to speed up a slow test suite starts with measurement, not random parallelism. In 2026, most slow suites are slow because of serial browser work, shared environments, oversized end-to-end coverage, chatty setup, and flaky retries that multiply runtime. Faster feedback is a reliability problem as much as a performance problem.

This guide gives working SDETs and QA leads a practical playbook: baseline the suite, find the real bottlenecks, apply the highest-leverage fixes first, and protect trust while you cut wall-clock time. You will get comparison tables, runnable configuration samples, and interview-ready reasoning.

TL;DR

Profile where time goes (setup, test body, teardown, queue, retries). Delete or demote low-value end-to-end cases, push checks down the test pyramid, isolate data, enable safe parallel workers, shard in CI, and cache dependencies. Never celebrate a shorter run that hides failures behind retries.

Lever Typical impact Risk if done poorly First metric to watch
Remove duplicate E2E High Coverage holes Critical path still guarded
Move checks to API/unit High False confidence Escaped defects in area
Parallel workers Medium-High Flakes, resource thrash First-attempt pass rate
CI sharding Medium-High Uneven shards P95 pipeline duration
Smarter waits / less sleep Medium New flakes Retry rate
Dependency caching Medium Stale tools Install time
Quarantine flakes Short-term Silent quality loss Quarantine age and owners

1. How to Speed up a slow test suite: Start With a Baseline

"The suite is slow" is not a diagnosis. Capture a baseline on a representative CI runner:

  • Wall-clock duration for the full pipeline job
  • Time to first test start (install, browser download, compile)
  • Per-test duration histogram (p50, p90, max)
  • Setup and teardown time if the runner exposes it
  • Retry count and time spent in retries
  • Queue time waiting for runners

Store results for at least a week of weekday runs so one noisy day does not mislead you. Tag the commit SHA, worker count, browser projects, and environment name with every measurement.

When people ask how to speed up a slow test suite, the first professional answer is: "We measured X minutes wall time, Y minutes in retries, Z minutes in install, and the top 20 tests consume about half the execution budget."

2. Separate Install Time From Test Time

Teams often optimize tests while the job spends eight minutes reinstalling browsers and packages on cold runners.

Fix install and tool bootstrap first when it dominates.

Runnable example: GitHub Actions cache for Playwright (Node)

# .github/workflows/e2e.yml
name: e2e
on:
  pull_request:
  push:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Cache Playwright browsers
        uses: actions/cache@v4
        id: playwright-cache
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

      - name: Install Playwright browsers
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: npx playwright install --with-deps chromium

      - name: Install system deps when cache hits
        if: steps.playwright-cache.outputs.cache-hit == 'true'
        run: npx playwright install-deps chromium

      - name: Run tests
        run: npx playwright test --reporter=line
        env:
          CI: "true"

Caching does not fix a 40-minute test body, but it often removes an easy multi-minute tax from every job.

3. Build a Duration Hit List

Export timings and sort. In Playwright, the HTML report and list reporter help; many teams also parse JUnit or JSON output into a spreadsheet.

Runnable example: print slowest tests from Playwright JSON report

// scripts/slowest-from-report.ts
import fs from "node:fs";

type Spec = {
  title: string;
  ok: boolean;
  tests?: Array<{
    results?: Array<{ duration?: number }>;
  }>;
};

type Suite = { title: string; suites?: Suite[]; specs?: Spec[] };

function walk(suite: Suite, prefix: string[] = [], out: Array<{ name: string; ms: number }>) {
  const path = suite.title ? [...prefix, suite.title] : prefix;
  for (const spec of suite.specs ?? []) {
    const durations = (spec.tests ?? []).flatMap((t) =>
      (t.results ?? []).map((r) => r.duration ?? 0),
    );
    const ms = Math.max(0, ...durations);
    out.push({ name: [...path, spec.title].filter(Boolean).join(" > "), ms });
  }
  for (const child of suite.suites ?? []) walk(child, path, out);
}

const report = JSON.parse(fs.readFileSync("playwright-report/report.json", "utf8"));
const rows: Array<{ name: string; ms: number }> = [];
for (const suite of report.suites ?? []) walk(suite, [], rows);
rows.sort((a, b) => b.ms - a.ms);
console.table(rows.slice(0, 20).map((r) => ({ seconds: (r.ms / 1000).toFixed(1), name: r.name })));

Run with JSON reporter configured, then:

npx tsx scripts/slowest-from-report.ts

Attack the top of the list first. Ten tests that each take three minutes often matter more than one hundred tests that each take three seconds.

4. Delete, Demote, or Split Low-Value End-to-End Tests

The highest leverage speedup is fewer expensive tests that add little unique signal.

Questions for each slow E2E case:

  • Does a unit or API test prove the same rule faster?
  • Is this duplicated across browsers without a browser-specific reason?
  • Does it set up a long journey only to assert one field at the end?
  • Has it failed for product bugs in the last six months, or only for flake?

Actions:

  • Delete obsolete journeys no product owner will defend.
  • Demote stable business rules to API or component tests.
  • Split god scenarios into focused paths with shared fixtures.
  • Tag deep journeys for nightly, keep smoke for PR.

Related: smoke vs sanity vs regression and add parallel execution to a framework.

5. Push Checks Down the Pyramid Without Losing Confidence

A slow suite often has inverted coverage: everything is UI.

Check type Relative cost Best for Weak for
Unit Very low Pure logic, parsing, pricing rules Full integration
Component Low UI state and rendering Backend workflows
API / contract Low-Medium Authz, validation, persistence Visual UX
UI E2E High Critical user journeys Exhaustive combinations

Runnable example: move a pricing rule to unit tests (TypeScript)

// src/pricing/applyCoupon.ts
export type Coupon = { type: "percent"; value: number } | { type: "fixed"; value: number };

export function applyCoupon(subtotalCents: number, coupon: Coupon): number {
  if (subtotalCents < 0) throw new Error("subtotal must be >= 0");
  if (coupon.type === "percent") {
    const discount = Math.floor((subtotalCents * coupon.value) / 100);
    return Math.max(0, subtotalCents - discount);
  }
  return Math.max(0, subtotalCents - coupon.value);
}
// src/pricing/applyCoupon.test.ts
import { describe, expect, it } from "vitest";
import { applyCoupon } from "./applyCoupon";

describe("applyCoupon", () => {
  it("applies percent coupons with floor rounding", () => {
    expect(applyCoupon(999, { type: "percent", value: 10 })).toBe(900);
  });

  it("never returns negative totals for fixed coupons", () => {
    expect(applyCoupon(500, { type: "fixed", value: 800 })).toBe(0);
  });
});

Keep one UI test that proves the coupon field wires to the backend. Delete five UI variants that only rechecked arithmetic.

6. Replace Sleeps With Deterministic Waits

Fixed sleeps are silent runtime taxes and flake factories.

Bad pattern

await page.getByRole("button", { name: "Save" }).click();
await page.waitForTimeout(5000);
await expect(page.getByText("Saved")).toBeVisible();

Better pattern (Playwright auto-wait + assertion)

await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Saved")).toBeVisible();

When you must wait on network

const responsePromise = page.waitForResponse(
  (res) => res.url().includes("/api/profile") && res.request().method() === "PUT",
);
await page.getByRole("button", { name: "Save" }).click();
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
await expect(page.getByText("Saved")).toBeVisible();

Across hundreds of tests, removing multi-second sleeps can reclaim large wall time without any new hardware.

7. Speed Up Setup: Auth, Seed Data, and Shared State

Login through the UI before every test is a common time sink.

Runnable example: Playwright storage state for authenticated tests

// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
import path from "node:path";

const authFile = path.join(__dirname, "../.auth/user.json");

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill(process.env.E2E_USER_EMAIL!);
  await page.getByLabel("Password").fill(process.env.E2E_USER_PASSWORD!);
  await page.getByRole("button", { name: "Sign in" }).click();
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
  await page.context().storageState({ path: authFile });
});
// playwright.config.ts (excerpt)
import { defineConfig } from "@playwright/test";

export default defineConfig({
  projects: [
    { name: "setup", testMatch: /auth\.setup\.ts/ },
    {
      name: "chromium",
      dependencies: ["setup"],
      use: { storageState: ".auth/user.json" },
    },
  ],
});

Prefer API seeding over UI wizards for test data. Create resources with unique IDs, and clean only what you own. See how to design a test data strategy.

8. Parallelize Safely After Isolation Exists

Parallelism multiplies bugs in shared accounts, static filenames, and singleton sandboxes. Prove isolation first, then raise workers.

Playwright config for local and CI workers

// playwright.config.ts (excerpt)
import { defineConfig } from "@playwright/test";

export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 4 : undefined,
  retries: process.env.CI ? 1 : 0,
  use: {
    baseURL: process.env.BASE_URL,
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
});

pytest-xdist for API suites

pytest -n 4 tests/api --dist=loadscope

Increase workers gradually: 1 -> 2 -> 4. Stop when first-attempt failures rise or the environment saturates. More on this in parallel test sharding in CI.

9. Shard Across CI Jobs When One Machine Is Not Enough

Workers help on one host. Shards help across hosts.

Runnable example: Playwright shard matrix

# .github/workflows/e2e-sharded.yml
jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - name: Run shard
        run: npx playwright test --shard=${{ matrix.shard }}

Balance matters. Equal test counts can still leave one shard late if long tests cluster. Use historical durations when your runner supports it, or split known long journeys across shards manually.

10. Cut Browser Matrix Cost

Running every test on Chromium, Firefox, and WebKit multiplies time. A common 2026 pattern:

  • PR: Chromium smoke only
  • Main branch: Chromium full suite + WebKit critical path
  • Nightly: full matrix for critical tags

Example project layout

projects: [
  { name: "pr-chromium", testMatch: /.*\.spec\.ts/, grep: /@smoke/, use: { browserName: "chromium" } },
  { name: "nightly-webkit", testMatch: /.*\.spec\.ts/, grep: /@critical/, use: { browserName: "webkit" } },
]

Do not pretend browser coverage is free. Choose it deliberately.

11. Manage Flakes and Retries Without Hiding Rot

Retries can make a suite "pass" while doubling runtime on bad days.

Rules:

  • Prefer fixing root causes over raising retry counts.
  • Keep PR retries low (0-1).
  • Preserve first-failure artifacts.
  • Quarantine only with an owner and expiry.
  • Track time spent in retries as a first-class cost metric.

Helpful related guide: how to reduce flaky tests in a CI pipeline and flaky test quarantine in CI.

12. Optimize Selectors, Fixtures, and Page Object Churn

Slow UI tests often navigate too much and query the DOM inefficiently.

  • Prefer role and label locators that resolve quickly and stably.
  • Avoid scanning huge tables when a direct row test id exists.
  • Cache page objects per test, not via global mutable singletons.
  • Navigate via deep links when the product supports them.
  • Stub non-essential third parties (analytics, chat widgets) when they are not under test.

Example: deep link instead of five setup screens

test("invoice detail shows tax line", async ({ page }) => {
  await page.goto("/app/invoices/inv_seed_tax_1");
  await expect(page.getByTestId("tax-total")).toHaveText("$8.00");
});

13. How to Speed up a slow test suite in Four Weeks

Use a time-boxed program so optimization does not become endless tinkering.

Week 1: Measure

  • Baseline install, execution, retries, top 20 slow tests
  • Classify failures: product, env, flake, obsolete

Week 2: Remove and demote

  • Delete obsolete E2E
  • Move pure rules to unit/API
  • Introduce or expand storage-state auth

Week 3: Parallel and shard

  • Fix isolation issues
  • Enable modest workers
  • Add shards if still over SLO

Week 4: Guardrails

  • Document suite SLOs (for example, PR smoke under 12 minutes)
  • Add a slow-test budget in code review
  • Schedule monthly duration review

Illustrative target (set your own): PR smoke under 15 minutes wall time, nightly under 60 minutes with shards, retry time under 10 percent of execution.

14. Communication and Stakeholder Tradeoffs

Speeding a suite is a product decision about risk vs feedback. Present options:

  1. Keep full coverage, buy more CI concurrency.
  2. Keep budget fixed, thin PR suite, deeper nightly.
  3. Invest engineering time to demote E2E and fix flakes.

Do not promise linear speedups. State assumptions: worker count, browser matrix, environment capacity. Re-measure after each major change.

Interview Questions and Answers

Q: How do you approach a test suite that takes two hours on CI?

I baseline wall time, install time, retries, and the slowest tests. I remove or demote low-value end-to-end cases, push logic checks down to unit and API layers, then parallelize only after isolation is proven. I set suite SLOs for PR versus nightly so the team optimizes the feedback path that matters most.

Q: What is the fastest way to reduce runtime without buying hardware?

Delete duplicate journeys, replace UI setup with API seeding and storage state, remove hard sleeps, and stop running the full browser matrix on every PR. Those changes often beat a raw worker increase.

Q: How do you parallelize without creating flakes?

Give each test unique data and artifact paths, avoid shared mutable accounts, and raise worker count gradually while watching first-attempt pass rate. Treat parallel-only failures as defects in isolation, not as reasons to spam retries.

Q: When is sharding better than more workers on one machine?

When a single host is CPU, memory, or browser-session bound, or when the suite still exceeds the job time budget after local parallelism is tuned. Shards scale horizontally across CI runners.

Q: How do retries affect suite speed?

Retries multiply runtime on unstable tests and can hide real failures. I keep retries low on PR, measure time spent retrying, and fix or quarantine root causes with owners.

Q: Which metrics prove the suite actually got faster in a useful way?

Wall-clock feedback time for the PR gate, p95 pipeline duration, first-attempt pass rate, retry time share, and whether critical coverage still exists. A shorter red-green flip that misses bugs is not a win.

Q: How do you stop the suite from slowing down again?

Add duration budgets in review, track the top slow tests monthly, require justification for new full E2E journeys, and keep pyramid guidance in the contribution docs.

Common Mistakes

  • Enabling large worker counts before fixing shared state.
  • Using retries as a performance and reliability strategy.
  • Running full cross-browser matrices on every pull request.
  • Logging in through the UI in every test.
  • Keeping obsolete end-to-end journeys because "we might need them."
  • Optimizing microbenchmarks while install time dominates.
  • Ignoring environment saturation and blaming the runner only.
  • Measuring only average duration and missing long-tail tests.
  • Stubbing away the very integration you claim to test.
  • Celebrating speed while artifact quality and failure diagnosis get worse.

Conclusion

Learning how to speed up a slow test suite is a disciplined optimization loop: measure, remove waste, push checks down, isolate, parallelize, and shard. Protect first-attempt reliability while you cut wall time. Fast feedback that the team trusts will change development behavior. Fast feedback that flakes will be ignored.

Start this week with a baseline and a top-20 slow test list. Ship one deletion or demotion, one setup improvement (auth or seeding), and one controlled parallelism change. Re-measure. That sequence beats a rewrite fantasy and compounds over a month into a suite people actually wait for.

15. Environment and Backend Costs You Can Actually Control

Not all slowness lives in the test runner. If every scenario hits a cold serverless function, a shared rate-limited sandbox, or a search index that rebuilds on write, your suite will plateau no matter how many workers you buy.

Practical controls:

  • Prefer stable dedicated non-prod stacks for CI over contended shared QA envs when budget allows.
  • Cache immutable reference data inside the environment seed, not inside each test.
  • Short-circuit third-party SaaS with contract-approved fakes when the third party is not the system under test.
  • Watch p95 API latency during the suite. If backend latency doubles under parallel load, fix capacity or reduce write-heavy scenarios.
  • Align test concurrency with known database connection pools and feature flag service limits.

Document environment SLOs next to suite SLOs. When someone asks how to speed up a slow test suite, include the sentence: "We checked whether the app under test is the bottleneck." That single habit prevents months of runner thrashing.

16. Worked Example: Cutting a 70-Minute Playwright Job

A team has a 70-minute Chromium job on pull requests.

Baseline findings

  • 8 minutes installing browsers without cache
  • 12 minutes in retries on 6 flaky tests
  • 15 minutes of UI login and onboarding before assertions
  • 10 tests longer than 90 seconds each, mostly admin setup wizards
  • Full suite runs on every PR including visual journeys

Changes shipped over three weeks

  1. Cache Playwright browsers and npm dependencies.
  2. Move login to storageState setup project.
  3. Demote pricing and permission rules to API tests.
  4. Tag visual journeys @nightly and remove them from PR.
  5. Fix 4 flakes, quarantine 2 with owners, set PR retries to 1.
  6. Enable 4 workers after unique account pools, then 2 shards.

Illustrative outcome shape (your results will differ): PR wall time falls into the teens of minutes, nightly absorbs deep coverage, and first-attempt pass rate rises because flake noise dropped. The important part is the order: measure, remove waste, then scale concurrency.

Guardrails in Code Review and CI Policy

Speed improvements evaporate without policy.

  • New @critical E2E tests require a one-line risk justification in the PR.
  • Tests over a duration threshold (for example 60 seconds) need a comment or a split plan.
  • Introducing waitForTimeout fails lint or code review unless there is a tracked exception.
  • Changing worker counts or retry defaults requires a short note in the testing ADR.
  • Nightly and PR project tags are enforced in config, not by memory.

These guardrails make how to speed up a slow test suite a continuous practice, not a one-off hero project after leadership complains about CI minutes.

Interview Questions and Answers

How do you approach a test suite that takes two hours on CI?

I baseline wall time, install time, retries, and the slowest tests. I remove or demote low-value end-to-end cases, push logic checks down to unit and API layers, then parallelize only after isolation is proven. I set suite SLOs for PR versus nightly so the team optimizes the feedback path that matters most.

What is the fastest way to reduce runtime without buying hardware?

Delete duplicate journeys, replace UI setup with API seeding and storage state, remove hard sleeps, and stop running the full browser matrix on every PR. Those changes often beat a raw worker increase.

How do you parallelize without creating flakes?

Give each test unique data and artifact paths, avoid shared mutable accounts, and raise worker count gradually while watching first-attempt pass rate. Treat parallel-only failures as defects in isolation, not as reasons to spam retries.

When is sharding better than more workers on one machine?

When a single host is CPU, memory, or browser-session bound, or when the suite still exceeds the job time budget after local parallelism is tuned. Shards scale horizontally across CI runners.

How do retries affect suite speed?

Retries multiply runtime on unstable tests and can hide real failures. I keep retries low on PR, measure time spent retrying, and fix or quarantine root causes with owners.

Which metrics prove the suite actually got faster in a useful way?

Wall-clock feedback time for the PR gate, p95 pipeline duration, first-attempt pass rate, retry time share, and whether critical coverage still exists. A shorter run that misses bugs is not a win.

How do you stop the suite from slowing down again?

Add duration budgets in review, track the top slow tests monthly, require justification for new full E2E journeys, and keep pyramid guidance in the contribution docs.

Frequently Asked Questions

What is the first step to speed up a slow test suite?

Measure a trustworthy baseline: wall-clock time, install time, retries, and the slowest tests on representative CI hardware. Without that baseline, teams usually add workers blindly and create flakes.

Does parallel execution always make tests faster?

No. Shared accounts, databases, and environments can cause contention that erases gains and raises failures. Prove isolation first, then increase workers gradually while watching first-attempt pass rate.

Should every test run on every browser in CI?

Usually not on every pull request. Use a thin PR matrix and deeper browser coverage on main or nightly for critical tags. Full matrices are expensive and should be intentional.

How do I speed up login-heavy UI suites?

Authenticate once per worker or suite using storage state or API tokens, then reuse that state. Avoid clicking through login screens before every scenario unless login itself is under test.

Are test retries a good way to manage duration?

Retries can hide instability and increase runtime on bad days. Keep retries low, fix root causes, and track time spent in retries as a cost metric.

When should I shard tests across multiple CI jobs?

When a single machine is saturated or the suite still exceeds your feedback SLO after removing waste and tuning workers. Shards scale horizontally across runners.

How can I keep the suite from slowing down again?

Set PR and nightly duration SLOs, review top slow tests monthly, require justification for new broad E2E journeys, and keep contribution docs aligned with the test pyramid.

Related Guides