Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

How to Reduce Test Execution Time by 60%: Practical Optimization Guide

April 5, 2026 EST. READ: 12 min read MIN #Quality Assurance

How to Reduce Test Execution Time by 60%: Practical Optimization Guide

Here's a hard truth: if your test suite takes 45 minutes to run, you've already lost.

Your developers won't run tests before pushing. Your CI/CD pipeline blocks for three-quarters of an hour. Your team ships slower. Bugs slip through. And every engineer blames "that slow test suite."

But slow tests aren't inevitable. They're a symptom of preventable inefficiency.

I've spent the last 18 months optimizing test suites across five major projects—from fintech platforms to AI-powered SaaS. The pattern is always the same: teams think they need a faster CI/CD server or a bigger testing budget. They don't. Most teams can cut test execution time by 50-70% without buying anything or hiring anyone.

This guide shows you exactly how.


Why Test Execution Time Matters (The Hidden Cost)

Let me show you what slow tests actually cost:

Scenario: Your test suite takes 45 minutes

  • Developer pushes code at 10:00 AM
  • Tests start running at 10:02 AM (after build)
  • Tests finish at 10:47 AM
  • Developer sees results at 10:50 AM
  • If tests fail: 15 minutes to investigate, fix, and re-run
  • Result: 1 hour + to get feedback on a 5-minute code change

Multiply this by:

  • 5 developers on your team
  • 8-10 commits per developer per day
  • 5 days per week
  • 52 weeks per year

Annual cost: ~2,600 hours of developer time waiting for tests. That's $300,000+ in lost productivity per year on a mid-size team.

And that's just the direct cost. The indirect costs are worse:

  • Developers stop running tests locally (they're too slow)
  • Bugs slip through CI/CD
  • Quality decreases as coverage decreases
  • Shipping velocity drops
  • Morale suffers ("our test suite is so slow...")

The good news: This is completely fixable.

On a recent SaaS project, we cut test execution from 45 minutes to 18 minutes. Same tests. Same application. Different strategy. Here's what we did.


The 5 Biggest Test Performance Killers

Before you optimize, understand what's killing your speed:

Killer #1: Serial Execution (Running Tests One at a Time)

The Problem: By default, most frameworks run tests sequentially. Test 1 finishes. Test 2 starts. If you have 100 tests taking 30 seconds each, that's 50 minutes. Total.

Real Impact: On our Wells Fargo fintech project, we had 150 tests running serially. Total execution: 47 minutes. Same tests, parallel: 12 minutes. That's 74% faster.

Killer #2: Slow Test Setup & Teardown

Every test spends 5 seconds creating test data, logging in, navigating to the feature. Multiply by 100 tests: 500 seconds of pure overhead.

Real Impact: One e-commerce QA team had each test create a new database record, verify it, delete it. 3 seconds per test. 100 tests = 5 minutes of wasted work. When we moved to shared test data + test isolation, that became 30 seconds total.

Killer #3: Bad Selectors & Flaky Waits

A single flaky test that occasionally fails forces a retry. That test that randomly times out? It's running twice. A test suite with just 10% flakiness effectively runs 10% of tests twice. 50 minutes × 1.1 = 55 minutes.

Real Impact: Before we fixed flaky selectors on our AI testing project, we had ~15% flakiness. Fixing selectors and switching to proper wait strategies: 35% time savings.

Killer #4: Running All Tests on All Browsers

You test on Chrome, Firefox, and Safari. That's 3x the execution time. For 100 tests:

  • Chrome: 12 minutes
  • Firefox: 12 minutes
  • Safari: 12 minutes
  • Total: 36 minutes

But you don't need all tests on all browsers. Some tests (visual/layout) need multi-browser coverage. Others (API tests) don't care about the browser.

Real Impact: Most teams can reduce browser coverage by 60-70% with smart tagging.

Killer #5: Unoptimized Test Database & Test Data

Every test waits for its data. If your test database is on a remote server 300ms away, and each test does 3 database calls, that's ~1 second of network latency per test. 100 tests = 100+ seconds of pure network waiting.

Real Impact: Moving test data closer (or using in-memory fixtures) saved one team 18% execution time.

These 5 killers compound. If you have all five, your test suite is likely 3-4x slower than it could be.

Let's fix them.


Low-Hanging Fruit: Quick Wins (20% Savings in 2 Hours)

These changes require zero architectural refactoring. Implement them this week.

Quick Win #1: Enable Parallel Test Execution

Playwright Configuration:

// playwright.config.ts
export default defineConfig({
  // Enable parallel execution
  fullyParallel: true,
  
  // Set workers = number of CPU cores (or less)
  workers: 4,
  
  // Optional: limit workers in CI (CI runners often have fewer cores)
  workers: process.env.CI ? 2 : 4,
});

GitHub Actions Configuration:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:playwright -- --shard=${{ matrix.shard }}/4

Time Savings: 40-50% (depends on number of CPU cores)

Quick Win #2: Skip Browser Combinations for Non-UI Tests

// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'chromium-ui',
      use: { ...devices['Desktop Chrome'] },
      testMatch: '**/ui/**', // Only UI tests on Chrome
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
      testMatch: '**/critical/**', // Only critical tests on Firefox
    },
    {
      name: 'api',
      use: {}, // API tests don't need browser
      testMatch: '**/api/**',
    },
  ],
});

Time Savings: 30-40% (skip 2 unnecessary browser runs)

Quick Win #3: Use Test Tags to Run Smoke Tests First

// Mark tests
test('should login @smoke @critical', async ({ page }) => { ... });
test('should process payment @critical', async ({ page }) => { ... });
test('should show help text @non-critical', async ({ page }) => { ... });

// Run only critical tests
// npx playwright test --grep @critical

// Run non-critical tests only after smoke passes
// npx playwright test --grep @non-critical

Time Savings: Not direct, but 95% feedback in 5 minutes instead of waiting 45 minutes. Huge for DX.

Quick Win #4: Cache Authentication

// Global setup
export async function globalSetup() {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = context.createPage();
  
  await page.goto('https://example.com/login');
  await page.fill('input[data-testid="email"]', 'test@example.com');
  await page.fill('input[data-testid="password"]', 'password123');
  await page.click('button[type="submit"]');
  await page.waitForURL('/dashboard');
  
  // Save authentication state
  await context.storageState({ path: 'auth.json' });
  await browser.close();
}

// Use in tests
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  use: {
    storageState: 'auth.json', // All tests reuse auth
  },
});

Result: No more logging in for every test. 5-10 seconds saved per test = 8-16 minutes for 100 tests.

Time Savings: 20% (eliminate repeated login)

Combined Quick Wins Total

Parallel execution (50%) + Skip browsers (35%) + Auth caching (20%) = ~70% time savings with zero architecture changes.

If you implement just these four changes, your 45-minute suite becomes a 13-minute suite.


Parallelization Strategy: From Serial to Parallel (30-40% Additional Savings)

Once you've enabled parallel execution, optimize how tests distribute work.

Strategy #1: Test Isolation & Independent Data

Problem: Tests share state. Test 1 creates a user. Test 2 needs that user. They can't run in parallel.

Solution: Each test creates its own data.

test('should allow user to update profile', async ({ page }) => {
  // Create unique user for this test
  const uniqueEmail = `user-${Date.now()}@test.com`;
  const userId = await createUserViaAPI(uniqueEmail, 'password123');
  
  // Test is now independent
  await page.goto(`/users/${userId}`);
  await page.fill('input[name="name"]', 'New Name');
  await page.click('button[type="submit"]');
  
  // Verify
  await expect(page.locator('.success-message')).toBeVisible();
});

Benefit: Tests run in any order, any time, in parallel. No race conditions.

Strategy #2: Database-Level Parallelization

If your tests hit a database, ensure:

  1. Test database is local (not remote)

    • Remote database = 100-500ms latency per query
    • Local (Docker) database = 10-50ms latency
    • Savings: 5-10 seconds per test
  2. Each test uses a separate schema/user

    # Dynamically create test schemas
    CREATE SCHEMA test_user_${RANDOM};
    USE test_user_${RANDOM};
    
  3. Clean up after tests (parallel-safe deletion)

    test.afterEach(async () => {
      // Clean only this test's data
      await db.query(`DELETE FROM users WHERE email = $1`, [uniqueEmail]);
    });
    

Real Impact: One e-commerce team cut database overhead from 8 seconds/test to 2 seconds/test.

Strategy #3: Distributed Test Execution Across Machines

For massive test suites (500+ tests), distribute across multiple CI workers:

# GitHub Actions matrix strategy
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        test_group: [smoke, ui, api, integration]
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:playwright -- --grep @${{ matrix.test_group }}

Result: 4 test groups × 4 workers = 16x parallelization potential.

Real-world impact on a 60-minute test suite:

  • Baseline serial: 60 minutes
  • Parallel on single machine (4 workers): 15 minutes
  • Distributed (16 workers): 4-5 minutes

Smart Test Prioritization (Run Less, Learn More)

Not all tests are equally valuable. Optimize which tests run when.

Strategy: Smoke → Critical → Full Suite

Smoke Tests (2 minutes)

  • Login
  • Basic navigation
  • Core functionality
  • Runs on every push

Critical Tests (10 minutes)

  • Payment processing
  • User authentication
  • API contracts
  • Security features
  • Runs on every push

Full Suite (45 minutes)

  • All tests
  • Runs on PRs before merge
  • Scheduled nightly

Workflow:

  1. Developer pushes code
  2. Smoke tests run (2 min) → feedback in 3 minutes
  3. If passes, critical tests run (10 min) → feedback in 13 minutes
  4. If passes, full suite runs (45 min) → feedback in 48 minutes
  5. Can merge after critical tests pass (13 min), full suite validates overnight

Psychology: Developers get feedback in 2 minutes instead of 45, so they run tests locally before pushing.

Implementation

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:playwright -- --grep @smoke
  
  critical:
    needs: smoke # Run only if smoke passes
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:playwright -- --grep @critical
  
  full:
    needs: critical
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run test:playwright

Time Savings: Not in total execution, but in perceived speed. Developers see results in 2-13 minutes, not 45.


Test Data & Setup Optimization (Eliminate Waste)

Problem: Expensive Test Data

Every test does:

  1. Create user → API call (1 second)
  2. Log in → UI interaction + API (3 seconds)
  3. Navigate to feature → 2 seconds
  4. Test actual feature → 5 seconds
  5. Cleanup → 2 seconds
    Total: 13 seconds

Only 5 seconds (38%) are actual feature testing. 62% is overhead.

Solution: Reusable Test Fixtures

// fixtures.ts
export async function createTestUser(context, uniqueId) {
  const response = await context.request.post('https://api.example.com/users', {
    data: {
      name: `Test User ${uniqueId}`,
      email: `test-${uniqueId}@example.com`,
      password: 'Test123!@',
    },
  });
  return await response.json();
}

// test.ts
test('should update profile', async ({ page, context }) => {
  const user = await createTestUser(context, Date.now());
  
  // Skip login, reuse auth
  await context.addCookies([
    {
      name: 'auth_token',
      value: user.token,
      url: 'https://example.com',
    },
  ]);
  
  // Direct to feature
  await page.goto(`/users/${user.id}/settings`);
  await page.fill('input[name="name"]', 'New Name');
  await page.click('button[type="submit"]');
  
  // Verify
  const updated = await context.request.get(`/users/${user.id}`);
  const userData = await updated.json();
  expect(userData.name).toBe('New Name');
});

Time Savings: From 13 seconds to 7 seconds per test (46% reduction).


Implementation Checklist: A 4-Week Plan

Week 1: Quick Wins (4 hours)

  • Enable parallel execution in Playwright config (30 min)
  • Configure GitHub Actions matrix (1 hour)
  • Add test tags (@smoke, @critical, @api) to existing tests (2 hours)
  • Implement auth caching (30 min)
  • Baseline measurement: Record current execution time
  • Expected Result: 45 min → ~15 min

Week 2: Parallelization (6 hours)

  • Audit test database setup (1 hour)
  • Implement test isolation (unique data per test) (2 hours)
  • Move test data to local/containerized database if needed (2 hours)
  • Re-measure execution time
  • Expected Result: 15 min → ~12 min

Week 3: Smart Prioritization (4 hours)

  • Identify smoke test cases (1 hour)
  • Identify critical test cases (1 hour)
  • Configure GitHub Actions workflow stages (2 hours)
  • Document test prioritization strategy for team
  • Expected Result: Fast feedback (2-13 min for critical tests)

Week 4: Optimization & Measurement (4 hours)

  • Identify and fix flaky tests (2 hours)
  • Optimize slow test data setup (1 hour)
  • Document final metrics and ROI
  • Train team on new workflow
  • Celebrate 60%+ time savings
  • Expected Result: 45 min → 18 min (60% faster)

Measuring Progress & ROI

Metric #1: Execution Time

Track in CI/CD logs:

# GitHub Actions output
Smoke tests: 2m 15s
Critical tests: 8m 30s
Full suite: 18m 45s

# vs Baseline
Before: 45m 00s
After: 18m 45s
Improvement: 58% faster

Metric #2: Developer Productivity

Before:

  • 10 developers × 10 commits/day = 100 commits/day
  • 45 min test execution = 75 hours waiting per day
  • Annual: 19,500 hours lost to waiting

After:

  • 100 commits/day
  • 13 min for feedback = 21.7 hours waiting per day
  • Annual: 5,630 hours saved

ROI: ~$200,000+ per year on a mid-size team (assuming $100/hr loaded cost)

Metric #3: Bug Prevention

Before: Slow tests = fewer local runs = more bugs in CI
After: Fast tests = more local runs = fewer bugs reaching CI

Track:

  • Bugs caught in CI/CD vs production
  • Percentage of developers running tests locally
  • Test failure rate (should stay stable, not increase)

Key Takeaways

  1. Slow tests are expensive — 45 minutes = ~$300,000/year in lost productivity
  2. Most slowness is preventable — 50-70% speedup is typical with no architectural changes
  3. Parallelization is the biggest lever — Enables 40-50% improvement alone
  4. Test prioritization matters — Feedback in 2 minutes >> feedback in 45 minutes
  5. 4 weeks to 60% improvement — Quick wins + parallelization + data optimization = massive ROI

Your team can cut test execution time by 60% in 4 weeks. No big refactoring. No expensive infrastructure. Just smart engineering.


Frequently Asked Questions

Q: Will parallelizing tests cause flakiness?

A: It can, but only if tests aren't isolated. If each test creates its own data and doesn't depend on other tests, parallelization is safe. We recommend auditing test isolation before going parallel.

Q: Should I run all tests in parallel?

A: Not always. Some tests intentionally run serially (e.g., database state tests, license tests). Use test tags to mark which tests can parallelize safely.

Q: How many parallel workers should I use?

A: Generally, number of CPU cores. For CI/CD, start conservative (2-4 workers) and increase if your infrastructure supports it.

Q: Does this apply to Cypress, WebDriver, or just Playwright?

A: The principles apply to all frameworks, but configuration is different. Playwright is the easiest to parallelize.

Q: What if my test database can't handle parallel access?

A: Either upgrade it, use separate test schemas per worker, or use in-memory databases (SQLite, H2) for testing.

Q: Will this reduce code coverage?

A: No, this improves speed without changing test count or coverage. You're running the same tests faster, not removing tests.


Next Steps

This Week:

  1. Measure your current test execution time (get baseline)
  2. Implement parallel execution and browser skipping (30 min)
  3. Re-measure (should see 40-50% improvement immediately)

Next Week:

  1. Implement auth caching and test data optimization
  2. Tag tests for prioritization
  3. Set up GitHub Actions matrix for distributed execution

Month 1:

  1. Complete 4-week implementation plan
  2. Measure final ROI
  3. Train team on new workflow

You can cut test execution time by 60% in 4 weeks. Let's do it.

Tayyab Akmal
// author

Tayyab Akmal

AI & QA Automation Engineer

6 years of catching critical bugs in fintech, e-commerce, and SaaS — then building the Playwright and Selenium automation that prevents them from shipping again.

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire