Cover image for Speed Up Test Executions: 5 Practical Ways

Speed Up Test Executions: 5 Practical Ways

Liviu Lupei

Liviu Lupei

Founder & Solutions Architect, Endtest · June 23, 2026

A slow automated test suite rarely becomes slow because of one dramatic technical mistake.

It usually happens one harmless-looking decision at a time.

Someone adds a five-second pause because a dropdown occasionally loads late. Then another person enables screenshots after every step. The staging environment gets moved to a smaller container to save money. Test cases start depending on whatever state the previous test left behind. Eventually, the suite takes 90 minutes, nobody wants to run it before a pull request, and the CI pipeline becomes something the team works around instead of something it trusts.

I have seen versions of this pattern across Selenium, Playwright, Cypress, custom testing frameworks, and intelligent test automation platforms. The syntax changes, but the bottlenecks are remarkably consistent.

The good news is that you usually do not need to rewrite the entire test suite. You need to identify where time is being spent, remove unnecessary work, and make the tests easier to distribute.

Here are the five changes that usually produce the largest improvement.

First, measure where the time is going

Before changing the framework, browser, or cloud provider, split the total execution time into a few basic categories:

  1. Time spent waiting in a queue for a browser
  2. Time spent starting the browser session
  3. Time spent preparing test data or authenticating
  4. Time spent performing browser actions
  5. Time spent waiting for application conditions
  6. Time spent capturing and uploading artifacts
  7. Time spent cleaning up after the test

This matters because teams often optimize the wrong layer.

A test that spends 40 seconds waiting for a report to appear will not become meaningfully faster because you replaced Selenium with Playwright. A suite that waits two minutes for every browser worker to start will not be fixed by changing a locator. A staging environment that takes eight seconds to return a basic API response will make every testing tool look slow.

Enable timestamps for individual steps whenever your testing tool supports them. Without step-level timing, you are guessing.

I usually start with the slowest ten tests, then inspect the slowest steps inside those tests. This gives you a much clearer picture than looking at the average duration of the entire suite.

1. Replace fixed pauses with condition-based waits

Hardcoded pauses are one of the most common sources of wasted test execution time.

Imagine that a test contains this instruction:

time.sleep(10)

The page may be ready after 800 milliseconds, but the test will still wait the full ten seconds. Multiply that by 30 pauses across 200 tests and you have created hours of waiting that produce no additional coverage.

A fixed pause is appropriate when the passage of time is actually part of the scenario. For example, you might intentionally wait to verify that a user session expires after a defined period. It can also be acceptable as a temporary debugging measure while you are figuring out which condition should be observed.

It should not be the default way to synchronize a test with the application.

The better pattern is:

  1. Define the condition that means the application is ready.
  2. Check that condition repeatedly.
  3. Continue immediately when it becomes true.
  4. Stop with a useful error after a maximum timeout.

The maximum timeout is important. Without it, a broken application can leave a test waiting for an unreasonable amount of time.

Selenium

In Selenium, use an explicit wait for a specific condition:

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

wait = WebDriverWait(driver, 10, poll_frequency=0.25)

checkout_button = wait.until(
    EC.element_to_be_clickable((By.ID, "checkout"))
)

checkout_button.click()

This does not mean the test always waits ten seconds. Ten seconds is the upper limit. If the button becomes clickable after 400 milliseconds, the test continues at that point.

Selenium's own waiting documentation describes explicit waits as polling loops that continue until a condition becomes true or the timeout is reached.

Be careful with a global implicit wait that is much larger than necessary. It can make every missing element expensive to diagnose, especially when combined with helper methods that perform multiple lookups.

Playwright

Playwright already performs actionability checks before many actions. For example, a locator click waits for the element to be visible, stable, enabled, and able to receive events.

That means this is often enough:

await page.getByRole("button", { name: "Checkout" }).click();
await expect(page.getByText("Order confirmed")).toBeVisible();

Playwright's auto-waiting and web-first assertions are useful precisely because they avoid the need for arbitrary sleeps.

The mistake I still see is adding waitForTimeout() around actions that already wait intelligently. That turns a fast, condition-based test back into a fixed-duration test.

Cypress

Cypress queries and assertions have built-in retry behavior. A chain such as this will keep retrying until it passes or reaches the configured timeout:

cy.get("[data-testid='order-status']")
  .should("contain.text", "Completed");

The Cypress documentation on retry-ability explains how queries and assertions are retried together.

The practical lesson is the same across all three libraries: wait for an observable result, not for a guessed number of seconds.

Endtest

In Endtest, use condition-based steps such as Wait Until or Wait For Element instead of adding a Pause after every navigation or click. You can also define a maximum wait time, so the test continues as soon as the condition is met and fails clearly when it is not.

The Endtest waits documentation covers the available options.

Endtest also records timestamps for test steps. Those timestamps are one of the fastest ways to identify a locator, page transition, API response, or assertion that is consuming more time than expected.

A practical rule is to review every pause longer than one second. For each one, ask: What exact condition are we really waiting for?

2. Collect fewer artifacts during routine runs

Artifacts are useful. They are also work.

Every screenshot requires the browser to render and capture pixels. Page source extraction requires the browser and driver to serialize the DOM. Network logs have to be collected, stored, transferred, and sometimes processed. Video recording adds encoding and upload work. Cookies, console logs, traces, and accessibility data all have a cost.

The bottleneck is not always the test framework. Often, the browser is doing exactly what you asked it to do.

A common overcorrection is to collect everything during every test:

  • Screenshot after every step
  • Full-page screenshot after every navigation
  • Page source after every action
  • Complete network log
  • Browser console log
  • Video
  • Trace
  • Cookies
  • Performance data

That might be reasonable for a short diagnostic run. It is usually excessive for a large regression suite that runs on every commit.

A better approach is to create artifact profiles.

Fast CI profile

Use this for pull requests and frequent regression runs:

  • Capture screenshots only on errors or failed assertions
  • Save page source only when a test fails
  • Keep essential console errors
  • Disable video unless it provides unique value
  • Record traces only on the first retry or failure
  • Avoid collecting full network traffic unless the test is specifically investigating network behavior

Diagnostic profile

Use this when reproducing an intermittent issue:

  • Screenshots at important checkpoints
  • Page source on selected steps
  • Full browser and console logs
  • Network capture
  • Video or trace
  • Additional timing information

Playwright explicitly recommends avoiding traces on every test because tracing every run is performance-heavy. A common Playwright configuration is to collect a trace only on the first retry:

import { defineConfig } from "@playwright/test";

export default defineConfig({
  use: {
    screenshot: "only-on-failure",
    video: "retain-on-failure",
    trace: "on-first-retry",
  },
});

Cypress automatically captures screenshots on failures during cypress run, while video recording can be enabled or disabled through configuration. Its screenshots and videos documentation also describes how teams can avoid retaining videos for successful specs.

In Selenium, artifact collection is usually implemented by the framework around WebDriver. The same principle applies: put screenshot, DOM, and network capture in failure hooks instead of calling them after every action.

In Endtest, artifact and logging behavior can be adjusted through the test suite's advanced settings. The right configuration depends on the purpose of the run.

Do not minimize artifacts at all costs. Collect the smallest set that still lets you understand a failure.

3. Give development and staging enough resources

Test automation is often blamed for application slowness that the test merely exposes.

Suppose the staging environment is running on a tiny container with aggressive CPU throttling, a cold database, limited memory, and background jobs competing for the same resources. A checkout flow that takes two seconds in production may take 15 seconds in staging.

The team then adds longer waits to the tests.

Now the tests are slower, the staging environment is still unrealistic, and genuine performance regressions become harder to distinguish from infrastructure noise.

Development and staging do not need to match the full production scale. They do need enough capacity to behave consistently and realistically for the traffic generated by the test suite.

Things worth checking include:

  • CPU throttling and memory pressure
  • Database connection limits
  • Cold starts
  • Autoscaling delays
  • Shared services used by multiple teams
  • Rate limits
  • Slow third-party sandbox APIs
  • Large debug builds or unoptimized frontend assets
  • Background jobs competing with tests
  • Test data that has grown far beyond normal production patterns

Using the smallest possible virtual machine or container can look like cost optimization. In practice, it can cost more through slow pipelines, false failures, repeated reruns, and engineering time spent investigating problems that do not occur in a properly sized environment.

There is also a trust problem. When everybody knows staging is slow and inconsistent, failures become easy to dismiss as "probably the environment." That is how real defects survive.

I recommend tracking basic application response times alongside test duration. If a test suddenly becomes slower, you should be able to see whether the time was spent inside the browser automation layer or waiting for the application itself.

For important end-to-end paths, keep the infrastructure profile reasonably close to production. You do not need to reproduce every production server, only to avoid testing under conditions so artificial that the results stop being useful.

4. Make tests independent, then run them in parallel

Parallel execution can turn a 60-minute suite into a 10-minute suite, but only when the tests are designed to run independently.

This is the part that gets skipped.

Teams sometimes enable ten workers on a suite whose tests share the same account, modify the same records, and assume that test case 7 will begin where test case 6 ended. The result is a faster way to create nondeterministic failures, not a faster suite.

A parallel-friendly test should be able to start without knowing what another test did before it.

For example, you should be able to run test cases 2, 6, 9, 10, and 13 at the same time without them changing each other's state.

That usually means each test should:

  • Navigate to a known starting location
  • Establish its own authentication state
  • Create or reserve the data it needs
  • Avoid modifying records used by another test
  • Clean up temporary data when appropriate
  • Use a separate browser session
  • Avoid depending on execution order

A checkout test might begin by opening the application, checking whether the user is authenticated, and logging in when necessary. It should not assume that a previous test already completed the login flow.

Do not duplicate the login logic in every test. Reuse it.

In Endtest, shared setup flows can be reused through imported test cases. Conditional logic can handle situations such as "log in only when the user is not already authenticated" through IF statements.

In Selenium, create a new WebDriver instance for each independently executed test and avoid shared mutable test data. Selenium's own guidance on avoiding shared state recommends isolated tests and separate driver instances.

In Playwright, test files already run in parallel by default, and worker count can be configured:

import { defineConfig } from "@playwright/test";

export default defineConfig({
  workers: process.env.CI ? 4 : undefined,
  fullyParallel: true,
});

For tests that modify server-side state, use separate accounts or data partitions for parallel workers. Reusing one authenticated state across tests is fast until two tests change the same user setting at the same time.

Cypress can distribute specs across parallel CI machines when configured with Cypress Cloud, but the same isolation rules apply. The orchestration layer cannot repair shared-state assumptions inside the tests.

One more point: if you started automating tests a few weeks ago, it is fine if the first tests are not perfectly independent. You are learning the application and the tool. Just treat independence as an architectural direction, not as something to postpone forever.

A useful migration approach is:

  1. Make the ten slowest or most important tests independent.
  2. Run those in parallel.
  3. Measure the reduction.
  4. Convert the rest of the suite in batches.

This produces value earlier than pausing all automation work for a large framework rewrite.

5. Use a cross-browser cloud built for execution

You can run browser tests on a developer laptop. You can also build your own Selenium Grid, provision CI containers, maintain browser images, install drivers, patch operating systems, manage queues, and investigate why a worker stopped accepting sessions at 2:00 a.m.

Sometimes self-hosting is the correct choice. It is not free.

The total test duration includes more than the time between the first click and the last assertion. It also includes:

  • Waiting for a worker
  • Starting the machine or container
  • Starting the browser
  • Establishing the remote session
  • Downloading dependencies
  • Uploading results
  • Replacing unhealthy workers

A setup that creates a fresh Windows worker for every test job can spend 30 to 60 seconds provisioning before the first test step runs. Across many short tests, startup time becomes a large percentage of the pipeline.

A mature cross-browser cloud keeps browser capacity ready, manages session allocation, and removes much of that provisioning delay.

It also makes parallel execution practical across browser and operating system combinations. That matters because production users do not all use the same headless Chromium build on Linux. Depending on your audience, reliable coverage may require Chrome and Edge on Windows, Safari on macOS, Firefox, mobile browsers, different resolutions, and different geographic locations.

Selenium Grid exists specifically to route tests to remote browser instances and run them in parallel across machines. Building and operating that Grid yourself, however, is a separate infrastructure project.

With Endtest, executions are routed to an always-available cross-browser cloud. In our internal launch-time measurements across the services and setups we benchmark, Endtest has delivered the fastest browser session startup we have measured, with many executions beginning in under one second.

That is internal product telemetry, not an independent industry benchmark, so I would not present it as a universal law. But startup time is measurable, and teams should measure it when comparing platforms.

Do not compare only how fast a single test runs after the browser is already open. Compare the complete CI experience:

  1. How long does the session wait in a queue?
  2. How long does browser startup take?
  3. How quickly can the suite scale from one to 20 parallel sessions?
  4. Does startup time become worse during busy periods?
  5. How much infrastructure maintenance is required?
  6. Can the same tests run on realistic browser and operating system combinations?

A fast cross-browser cloud does not fix slow test logic. It removes infrastructure overhead after the test logic has been cleaned up.

You can learn more about Endtest plans and parallel execution capacity on the pricing page.

A practical speed audit for your test suite

If your suite feels slow, do this before switching frameworks:

Day 1: Establish the baseline

Record:

  • Total suite duration
  • Queue time
  • Browser startup time
  • Average test duration
  • Ten slowest tests
  • Ten slowest individual steps
  • Artifact upload time
  • Retry count

Day 2: Remove obvious waiting

Search the suite for:

  • sleep
  • waitForTimeout
  • cy.wait with a numeric duration
  • Pause steps
  • Global timeouts that are much larger than necessary

Replace each fixed delay with a condition when possible.

Day 3: Reduce default artifacts

Create a fast CI profile that captures screenshots, DOM, video, traces, and network logs only when they provide diagnostic value.

Day 4: Check the application environment

Compare application response times in staging and production-like environments. Look for resource throttling, cold starts, database contention, and slow sandbox services.

Day 5: Parallelize a clean subset

Choose tests that already have good isolation. Run them with two workers, then four, then more. Watch for shared data collisions and diminishing returns.

Do not assume that doubling the number of workers will always cut the duration in half. The application, database, test data service, or account limits may become the new bottleneck.

Speed should improve feedback, not hide problems

Faster test execution should shorten the time between a code change and a trustworthy answer, not produce an impressive dashboard.

A five-minute suite that misses failures is not better than a 20-minute suite that catches them. A highly parallel suite that fails randomly because tests share state is noisy, not fast. A test that skips artifacts entirely may be quick, but expensive to investigate when it fails.

The best optimization is usually a balance:

  • Wait only as long as the application needs
  • Collect enough evidence to debug failures
  • Test against a stable and realistic environment
  • Design tests to run independently
  • Use execution infrastructure that starts quickly and scales predictably

Framework debates tend to focus on syntax and benchmark examples. In real projects, the biggest gains usually come from removing fixed waiting, reducing unnecessary browser work, improving test isolation, and eliminating infrastructure startup delays.

Those changes help whether your tests are written in Selenium, Playwright, Cypress, or built with an intelligent testing platform.

And unlike a rewrite, you can start applying them today.

Get started with Endtest today!

Create your first test in minutes, with nothing to install. Build fast, maintainable test suites without writing a line of code.