PRACTICAL GUIDE / FluentWait polling interval CPU usage

When a fast FluentWait poll makes Selenium slower

Measure Selenium wait callback cost, separate local CPU pressure from remote command load, and choose a polling interval that holds up in parallel CI.

By The Testing AcademyUpdated August 4, 202624 min read
All field guides
In this guide6 sections
  1. Why a short interval can create more work
  2. How to prove polling is the pressure source
  3. Three failures that need different fixes
  4. A cheap condition multiplied across parallel sessions
  5. A table scan that is expensive on every attempt
  6. An implicit wait hiding inside the explicit wait
  7. A browser or application bottleneck that polling cannot repair
  8. How to choose and implement the fix
  9. How to roll the change through CI
  10. When a faster poll is the wrong move

What you will learn

  • Why a short interval can create more work
  • How to prove polling is the pressure source
  • Three failures that need different fixes
  • How to choose and implement the fix

Your Selenium jobs pass on a laptop, then a parallel CI shard drives the browser workers hard enough that every checkout test starts timing out. The shared wait helper checks the page every 25 milliseconds, and each check sends several WebDriver calls. Shortening the interval made the suite less responsive, not more.

Questions about FluentWait polling interval CPU usage often collapse four different costs into one label. The Java test process does some work, the driver and browser do some work, a remote Grid may queue commands, and the application still has to reach the state being tested. A useful diagnosis separates those boundaries before anyone changes another timeout.

Why a short interval can create more work

A FluentWait is a loop around a condition function. Selenium applies the function to its input and succeeds when that function returns a value that is neither null nor false. An unignored exception ends the wait immediately. An ignored exception allows another attempt, while an unsuccessful value keeps the loop alive until the timeout expires or the thread is interrupted. Those outcomes are part of the documented FluentWait contract, not framework folklore.

The polling interval controls the pause between evaluation loops. It does not schedule callbacks at an exact fixed rate. Selenium's Java API documentation says the real interval can be greater because the time spent evaluating the condition is not included in the configured value. Put more concretely, one loop evaluates the callback, checks whether time has expired after an unsuccessful result, and then sleeps before the next evaluation. A callback that takes 180 milliseconds followed by a 200 millisecond sleep produces a start-to-start gap of roughly 380 milliseconds, plus scheduling overhead. That arithmetic is illustrative, not a benchmark.

One wait also runs its condition sequentially. Its second evaluation does not overlap its first evaluation. A 10 millisecond interval cannot launch a new check while the previous callback is still waiting for a browser response. Parallelism enters when a test runner executes many sessions, when several suite processes run on the same worker, or when the application itself performs concurrent work. Independent waits can all become eligible to poll frequently even though each individual wait remains sequential.

That distinction matters for CPU claims. During the configured pause, the waiting Java thread is sleeping rather than spinning continuously. A shorter pause creates more opportunities to run the condition, but it does not prove that the Java process will consume materially more CPU. A condition dominated by remote I/O may leave the client mostly waiting. The same cadence can still increase request handling, element searches, script execution, browser main-thread work, driver work, or Grid traffic elsewhere. Measure the component you plan to blame.

The callback determines the cost of each opportunity. Consider a check that locates a status region, asks whether it is displayed, reads two attributes, and then scans child rows. The code crosses the Selenium API boundary several times on every unsuccessful attempt. A condition based on one narrow element query does less work. Both use the same pollingEvery value, yet their operational effect can be very different.

There is also no universal meaning for "fast." A locally hosted static test page, a browser on the same machine, and a remote browser behind a Grid have different command latency. A CI worker running one session behaves differently from a Grid node sharing resources among sessions. Copying a 50 millisecond value from a unit-test polling utility into a WebDriver wait ignores those differences.

Timeout and cadence solve different problems. The timeout is the maximum waiting budget expressed to FluentWait. The polling interval controls how soon another condition evaluation may happen after an unsuccessful one. Increasing the timeout gives a genuinely slow product transition more time; shortening the interval only checks more often when the callback is cheap enough to finish. Neither setting repairs a locator that never matches, a request that never completes, or a state transition the test failed to trigger.

One final mechanism catches teams during migration: WebDriverWait is a specialization of FluentWait. Its Java API documentation says it ignores NotFoundException from the condition by default and propagates other exceptions unless more are added. A plain FluentWait should not be treated as interchangeable with every helper built on WebDriverWait. Broadly ignoring exceptions can make an expensive loop continue when the correct behavior is to fail on the first automation error.

How to prove polling is the pressure source

Begin at the condition boundary. Log when each evaluation starts, how long it takes, what it returns, and which exception escapes. The start gap tells you the observed cadence. The condition duration tells you whether callback work dominates the configured sleep. The poll count gives you a denominator for any method-call count. Without those three values, a CPU chart is only correlated with the test run, not with the wait.

The following diagnostic keeps the condition intentionally narrow. It uses findElements so absence is represented by an empty list rather than an exception. It does not ignore StaleElementReferenceException or other failures, because swallowing them would change the test while it is being measured.

Java
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;

AtomicInteger pollCount = new AtomicInteger();
AtomicLong previousStart = new AtomicLong(-1L);

Wait<WebDriver> readiness =
    new FluentWait<WebDriver>(driver)
        .withTimeout(Duration.ofSeconds(12))
        .pollingEvery(Duration.ofMillis(300))
        .withMessage(() -> "checkout marker did not reach data-state=ready");

readiness.until(
    current -> {
      int poll = pollCount.incrementAndGet();
      long started = System.nanoTime();
      long previous = previousStart.getAndSet(started);
      long startGapMs =
          previous < 0
              ? -1
              : TimeUnit.NANOSECONDS.toMillis(started - previous);

      try {
        List<WebElement> markers =
            current.findElements(By.cssSelector("[data-test='checkout-status']"));
        boolean ready =
            markers.size() == 1
                && "ready".equals(markers.get(0).getDomAttribute("data-state"));

        long conditionMs =
            TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
        System.out.printf(
            Locale.ROOT,
            "wait=checkout-ready poll=%d startGapMs=%d conditionMs=%d result=%s%n",
            poll,
            startGapMs,
            conditionMs,
            ready);
        return ready;
      } catch (RuntimeException failure) {
        long conditionMs =
            TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
        System.out.printf(
            Locale.ROOT,
            "wait=checkout-ready poll=%d startGapMs=%d conditionMs=%d result=threw exception=%s%n",
            poll,
            startGapMs,
            conditionMs,
            failure.getClass().getName());
        throw failure;
      }
    });

A healthy interpretation is more useful than a preferred number. Suppose an illustrative log shows condition durations around 20 milliseconds and start gaps around 320 milliseconds with a 300 millisecond interval. That shape agrees with the documented model: callback time plus sleep. If the same condition instead takes about two seconds per attempt, changing the sleep from 300 to 100 milliseconds barely changes the evaluation rate. The expensive condition, an implicit wait, remote latency, or a slow browser command now deserves attention.

Poll logs do not tell you how much Selenium-facing work each callback performs. Selenium provides WebDriverListener and EventFiringDecorator for observing calls on a decorated driver and objects derived from it. The API describes before, after, and error events, including generic hooks for WebDriver and WebElement methods. The counter below reports decorated method calls, not wire-protocol commands. One decorated method can involve implementation details the listener does not expose, so do not relabel this count as network requests.

Java
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.concurrent.atomic.LongAdder;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.events.EventFiringDecorator;
import org.openqa.selenium.support.events.WebDriverListener;
import org.openqa.selenium.support.ui.FluentWait;

final class SeleniumCallCounter implements WebDriverListener {
  private final LongAdder webDriverCalls = new LongAdder();
  private final LongAdder webElementCalls = new LongAdder();

  @Override
  public void beforeAnyWebDriverCall(
      WebDriver driver, Method method, Object[] args) {
    webDriverCalls.increment();
  }

  @Override
  public void beforeAnyWebElementCall(
      WebElement element, Method method, Object[] args) {
    webElementCalls.increment();
  }

  void reset() {
    webDriverCalls.reset();
    webElementCalls.reset();
  }

  long webDriverCalls() {
    return webDriverCalls.sum();
  }

  long webElementCalls() {
    return webElementCalls.sum();
  }
}

SeleniumCallCounter counter = new SeleniumCallCounter();
WebDriver observedDriver =
    new EventFiringDecorator<WebDriver>(counter).decorate(rawDriver);

counter.reset();
try {
  new FluentWait<WebDriver>(observedDriver)
      .withTimeout(Duration.ofSeconds(12))
      .pollingEvery(Duration.ofMillis(300))
      .until(
          current ->
              !current
                  .findElements(
                      By.cssSelector("[data-test='checkout-status'][data-state='ready']"))
                  .isEmpty());
} finally {
  System.out.printf(
      "wait=checkout-ready driverMethods=%d elementMethods=%d%n",
      counter.webDriverCalls(),
      counter.webElementCalls());
}

Use the decorated driver for the condition being investigated. If the helper silently closes over rawDriver, the listener will miss those calls and the count will be misleading. Reset immediately before the wait and read immediately after it so navigation, screenshots, and teardown do not pollute the result. The official WebDriverListener and EventFiringDecorator pages define the hooks used here. EventFiringDecorator is marked Beta in the current API, which is a reason to keep this instrumentation small and version-aware.

Now add component evidence. At the client boundary, capture wall time and process CPU for the focused test, Java Flight Recorder or another approved profiler if your team already uses one, and the per-poll diagnostics. At a remote execution boundary, retain the Selenium session ID, Grid timestamps or metrics available in your deployment, and node-level browser or driver observations. At the application boundary, retain the request or job identifier that proves when the awaited transition actually completed.

Do not infer remote saturation from Java CPU alone. A low client CPU value paired with rising callback duration can fit network delay, Grid queueing, a busy browser, or a slow application. Conversely, high client CPU with stable remote command time can point to heavy condition-side parsing, aggressive logging, repeated page-source processing, or unrelated work in the same test JVM. The poll interval is causal only when changing that interval changes the relevant work while the condition, data, concurrency, browser, and application transition stay controlled.

A small evidence matrix prevents premature fixes:

ObservationPoll pressure is plausible whenA competing cause remains when
Poll countThe count rises as the interval falls under the same transitionCallback duration changes or the product completes at a different time
Condition durationIt stays comparable while more evaluations runIt expands enough to dominate the configured sleep
Decorated method countCalls per wait rise with added pollsSetup or teardown leaked into the counter
Client process CPURepeat runs move with cadence while other inputs stay fixedBrowser, Grid, and application timings also changed
Grid or driver latencyMore sessions and more calls coincide with queue pressureThe application response itself slowed first
Total wait elapsedThe state becomes ready but detection is delayed by cadenceThe state never becomes ready at all

Treat that table as a hypothesis guide. None of its rows is an automatic verdict. The proof comes from a controlled comparison and artifacts that share the same test attempt and Selenium session.

Three failures that need different fixes

A cheap condition multiplied across parallel sessions

A checkout page exposes one status marker after pricing completes. The condition performs one findElements call and one attribute read. It is cheap enough that a single local session does not look suspicious, so a framework author chooses a 25 millisecond interval to make the test feel responsive.

The problem appears only when the CI runner opens many sessions. Every unfinished checkout wait gets another chance to evaluate shortly after its prior callback completes. Even if each callback is individually modest, the combined decorated call count can grow. A shared Grid may spend more time serving status checks while the application and browser are also processing checkout work. The local reproduction misses the resource-sharing boundary.

A practical fix keeps the condition narrow and passes an explicit, reviewed cadence rather than burying a tiny duration in a universal helper. The helper below reacquires the marker on every evaluation, verifies that exactly one marker is visible, and returns that element only when aria-busy is no longer true. It validates the durations supplied by suite code. No Selenium configuration key is implied.

Java
import java.time.Duration;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;

public final class CheckoutReadiness {
  private static final By ROOT =
      By.cssSelector("[data-test='checkout-summary']");

  private CheckoutReadiness() {}

  public static WebElement await(
      WebDriver driver, Duration timeout, Duration pollInterval) {
    if (timeout.isZero() || timeout.isNegative()) {
      throw new IllegalArgumentException("timeout must be positive");
    }
    if (pollInterval.isZero() || pollInterval.isNegative()) {
      throw new IllegalArgumentException("poll interval must be positive");
    }

    return new FluentWait<WebDriver>(driver)
        .withTimeout(timeout)
        .pollingEvery(pollInterval)
        .withMessage("checkout summary remained busy")
        .until(
            current -> {
              List<WebElement> roots = current.findElements(ROOT);
              if (roots.size() != 1) {
                return null;
              }

              WebElement root = roots.get(0);
              if (!root.isDisplayed()) {
                return null;
              }

              return "true".equals(root.getDomAttribute("aria-busy"))
                  ? null
                  : root;
            });
  }
}

The cost is detection latency. If the state changes just after an unsuccessful check, Selenium will not see it until the callback gets another turn after the sleep. A longer interval can therefore add visible time to successful tests. That cost should be compared with the observed transition time and shared execution pressure, not hidden behind a claim that one value is best.

This fix also refuses to ignore every stale or JavaScript-related exception. A stale reference between locating the root and reading it is a different automation failure. Teams may choose to retry StaleElementReferenceException for a DOM that legitimately replaces the marker, but that choice changes what the wait tolerates and should be covered by a focused test. It should not arrive as collateral damage from a performance edit.

A table scan that is expensive on every attempt

An order-history test waits until every row leaves a pending state. Its first implementation locates every row, calls getText on each row, reads a class from each icon, and searches each row for a badge. The callback is effectively a repeated audit of the page. A shorter interval magnifies the audit, but the design of the predicate is the larger problem.

Move the expensive detail checks after readiness. During the wait, ask the smallest question that proves the transition: is the grid present, and are any pending rows left? A CSS selector can let the browser locate pending rows in one Selenium findElements operation. After the wait succeeds, the test can assert final row contents once.

Java
import java.time.Duration;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;

By gridLocator = By.cssSelector("[data-test='order-grid']");
By pendingLocator =
    By.cssSelector("[data-test='order-row'][data-state='pending']");

new FluentWait<WebDriver>(driver)
    .withTimeout(Duration.ofSeconds(20))
    .pollingEvery(Duration.ofMillis(400))
    .withMessage("order rows did not leave the pending state")
    .until(
        current -> {
          List<WebElement> grids = current.findElements(gridLocator);
          if (grids.size() != 1 || !grids.get(0).isDisplayed()) {
            return false;
          }
          return current.findElements(pendingLocator).isEmpty();
        });

List<WebElement> finalRows =
    driver.findElements(By.cssSelector("[data-test='order-row']"));
if (finalRows.isEmpty()) {
  throw new AssertionError("no completed order rows were rendered");
}
for (WebElement row : finalRows) {
  String state = row.getDomAttribute("data-state");
  if (!"complete".equals(state)) {
    throw new AssertionError("unexpected terminal row state: " + state);
  }
}

The grid check prevents a false pass before the table loads. The final assertion prevents an empty-but-present grid from being accepted as a completed order. Those guards come from the product contract. A generic helper cannot invent them safely.

The trade-off is locator coupling. The optimized condition depends on a stable data-state attribute and a reliable grid marker. If the product does not expose a compact readiness signal, adding a test-oriented attribute may be better than repeating dozens of element reads. That product change has ownership and maintenance cost, but it can also clarify accessibility and UI state semantics if designed with the frontend team.

A longer interval alone would reduce the number of scans but keep each scan wasteful. A timeout increase would allow even more expensive scans before failure. Neither addresses the callback's command shape. Count calls per poll before and after the predicate rewrite so reviewers can see that the optimization came from less work, not from weaker assertions.

An implicit wait hiding inside the explicit wait

The logs show a FluentWait with a 200 millisecond interval evaluating only once every several seconds. Someone concludes that Selenium ignored pollingEvery, or that the CI worker lacks CPU. The actual session has a nonzero implicit wait, and the callback uses findElement for an element that is absent.

An implicit wait applies to every element location call for the session. Selenium's waiting strategies guide says the default is zero, explains that a configured implicit wait delays a failed lookup, and explicitly warns against mixing implicit and explicit waits because elapsed times can become unpredictable. The FluentWait cannot start its sleep until the current findElement call returns or throws. Callback duration, rather than the poll interval, sets most of the observed cadence.

The distinguishing evidence is straightforward. The per-poll log shows a long condition duration. The decorated listener shows one findElement call rather than a rapid series. Reducing pollingEvery does not materially change start gaps. In a controlled reproduction, setting the session's implicit wait to zero before the explicit-wait scenario changes the callback shape. Do not make that change in the middle of a shared session without restoring the suite's contract, and do not use it as an unreviewed production workaround.

The durable fix is to choose one synchronization strategy for that boundary. With the session's implicit wait set to zero, an explicit-wait predicate can state the exact readiness condition and return false or null promptly when an element is absent. findElements is useful for that pattern because an empty result expresses absence without relying on an ignored NoSuchElementException. If the suite depends on an implicit wait elsewhere, migration needs an inventory of raw findElement calls before the global setting is removed.

This near-miss has the same surface symptom as an expensive condition: long gaps and an overall timeout. The exception chain and callback timing separate them. An ignored NoSuchElementException after a long lookup points toward wait interaction. A callback that returns false after many successful element and text calls points toward condition cost. A product request that never completes points somewhere else again.

A browser or application bottleneck that polling cannot repair

A fourth case deserves a place beside the three common ones. A page may be rendering a large component tree, decoding media, waiting for a server job, or repeatedly retrying an application request. The Selenium condition is only the observer. Making it poll less often may reduce observer load, but it does not make the product transition complete.

Check the browser and application evidence. Did the awaited DOM state appear before the timeout? Does the correlated application request finish? Do callback durations grow even when the interval and condition stay fixed? Does the same transition remain slow when exercised manually or through a non-Selenium probe? These questions keep an automation tuning task from concealing a real performance defect.

The correct outcome might be two changes. The test can adopt a less aggressive cadence to stop adding avoidable work, while the product team investigates the slow transition. Report those as separate findings. Calling the test fix a product performance fix would overstate what the evidence proves.

How to choose and implement the fix

Choose a cadence from observed product timing and callback cost. Start with the state transition the test cares about. Record when the triggering action completes, when the product exposes readiness, when each condition begins, and how long the condition runs. A reader should be able to see whether the interval affects only detection or whether repeated checks materially affect the system.

Simple arithmetic is useful for planning, as long as it is labeled as arithmetic. With a 10 second timeout, an immediate first evaluation, a negligible callback, and ideal scheduling, a 50 millisecond sleep allows roughly 201 evaluations before the timeout is observed. A 500 millisecond sleep allows roughly 21. These are derived illustrations, not Selenium benchmark results. Real counts can be lower because callbacks take time, scheduling is imperfect, the condition may succeed early, and remote commands may block.

Detection latency is the other side of the trade. A state that becomes ready just after a failed evaluation waits for another opportunity. Under a purely illustrative assumption that readiness occurs uniformly within a sleep interval and callback cost is negligible, the added detection delay averages about half the interval. Real applications do not promise a uniform distribution, so use the measured readiness pattern instead of presenting that average as a suite result.

Reduce callback cost before chasing tiny intervals. Reacquire only the elements needed for the current state. Prefer a stable readiness marker over reading the whole page. Move detailed assertions out of the loop. Avoid screenshots, full page source, large JSON parsing, and verbose artifact writes on every poll unless the investigation specifically needs them. Capture rich evidence once on timeout or on the final state.

Then group waits by behavior rather than assigning a unique number at every call site. A suite might maintain profiles for cheap DOM readiness, slower background jobs, and remote high-cost conditions. The profile names should express their purpose, and the underlying durations should remain visible in one reviewed location. Do not name profiles "fast," "normal," and "slow" without defining the condition cost and product transition they represent.

If a runtime override helps a rollout, make it clear that the property belongs to your suite. The code below reads a Java system property called wait.poll.ms, validates it, and passes the resulting Duration into the helper. Selenium does not define that property.

Java
import java.time.Duration;

long pollMillis =
    Long.parseLong(System.getProperty("wait.poll.ms", "300"));
if (pollMillis < 1 || pollMillis > 5_000) {
  throw new IllegalArgumentException(
      "wait.poll.ms must be between 1 and 5000");
}

Duration checkoutTimeout = Duration.ofSeconds(12);
Duration checkoutPoll = Duration.ofMillis(pollMillis);

CheckoutReadiness.await(driver, checkoutTimeout, checkoutPoll);

The upper bound is a suite policy in this example, not a Selenium limit and not a recommendation for every application. State that distinction in code review. Otherwise someone will eventually quote the validation range as framework behavior.

Do not tune the timeout and interval together in the first comparison. If both change, fewer polls could come from the larger sleep, the shorter timeout, an earlier product completion, or a changed callback duration. Hold the timeout steady while evaluating cadence. Once the condition cost is acceptable, set the timeout from the longest product behavior the test intentionally covers, plus whatever policy your team uses for environmental variance.

Concurrency is part of the configuration. A cadence that is harmless with two sessions can become costly when the runner opens many. Recheck after changes to shard count, browser count per node, video capture, network topology, or Grid capacity. That is not a call for a permanent microbenchmark. It is a call to treat execution architecture as an input whenever the suite's command pattern changes.

How to roll the change through CI

Start with inventory, not a global search-and-replace. Find constructors of FluentWait and WebDriverWait, wrapper methods that return Wait objects, calls to pollingEvery, and conditions that perform several WebDriver or WebElement operations. Include helpers that set implicit waits. Record which tests share each helper and whether they run locally or through a remote Grid.

Add poll timing and the decorated method counter in observation mode. It should write artifacts without changing pass or fail decisions. Scope the listener to selected waits or tag its output with the wait name, test attempt, and Selenium session ID. Redact page data and arguments if logs could contain credentials or personal information. A call count does not require logging every argument.

Select representative journeys instead of the fastest smoke test. Include one condition that succeeds quickly, one that normally waits for a real asynchronous transition, and one timeout case. Run them at the same concurrency used by the affected CI shard. Reset product data between candidates so an earlier run cannot warm or complete work for the next one.

This Linux shell script runs a focused Maven test with three candidate values. The values are experimental inputs, not claimed optimums. GNU time writes process-level resource observations for the Maven command, while the test's own log contains poll and listener diagnostics. Read those sources together and do not treat the time file as browser or Grid attribution.

Shell
set -euo pipefail

mkdir -p target/wait-probes

for interval in 700 100 300; do
  log_file="target/wait-probes/test-$interval.log"
  time_file="target/wait-probes/time-$interval.txt"
  /usr/bin/time -v -o "$time_file" mvn -q -Dtest=CheckoutWaitIT -Dwait.poll.ms="$interval" test > "$log_file"
done

grep -H -E "User time|System time|Percent of CPU|Elapsed" target/wait-probes/time-*.txt
grep -H "wait=checkout-ready" target/wait-probes/test-*.log

Repeat candidates enough times for your environment and rotate their order. Browser startup, driver download, caches, test data, and neighboring jobs can swamp the difference from one wait. Report the raw observations and the comparison method. Do not manufacture a clean percentage improvement from noisy runs.

Keep the first failing attempt. Retries can turn an overloaded or slow attempt into a green final result while deleting the only useful timing shape. Store per-attempt logs under separate names. If the CI platform reports only the last attempt, change artifact naming before using retries in this investigation.

A small canary job is safer than changing every shard on the same commit. The following GitHub Actions workflow wires the suite-owned system property into one focused test and retains the diagnostics. It assumes the repository's Maven project and test already exist.

YAML
name: wait-cadence-canary

on:
  workflow_dispatch:

jobs:
  checkout-wait:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    env:
      WAIT_POLL_MS: "300"
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"
          cache: maven
      - name: Run checkout wait canary
        run: |
          mkdir -p target/wait-probes
          /usr/bin/time -v -o target/wait-probes/process-time.txt mvn -q -Dtest=CheckoutWaitIT -Dwait.poll.ms="$WAIT_POLL_MS" test | tee target/wait-probes/test.log
      - name: Retain wait evidence
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: checkout-wait-evidence
          path: target/wait-probes/

Roll out in layers after the canary. Change one helper, watch the tests that depend on it, then expand to another condition family. Keep a temporary escape property only while the rollout needs it. A permanent global override makes local and CI behavior diverge too easily, especially when developers forget which value the build server injects.

Gate on test meaning first. A candidate interval is unacceptable if it misses a transient state the test is supposed to observe, turns a real failure into a pass, or stretches critical feedback beyond the team's budget. Resource evidence matters, but a lower CPU line does not compensate for weaker coverage.

Define rollback before expanding. Preserve the previous reviewed profile, the tests that exercise readiness near timeout, and the diagnostic switch. If Grid latency or suite duration regresses after rollout, restore the profile while retaining the new evidence. A rollback is much faster when it changes one owned configuration point rather than dozens of copied durations.

When a faster poll is the wrong move

Do not shorten the interval to repair a wrong condition. If the locator matches a spinner that never disappears because the selector is stale, more evaluations repeat the same mistake. If the click that starts the transition never happened, polling cannot create it. If the application uses a different completion signal, the test must wait for that signal.

Do not use cadence to hide mixed waits. A long implicit lookup inside an explicit wait needs a synchronization decision, not a 1 millisecond polling value. The shorter sleep is mostly irrelevant when each callback blocks for far longer. It can also make the suite noisier once the element begins returning promptly.

Do not assume a longer interval is always safer, either. A test may intentionally observe a brief state, such as a progress message required by the product contract. If that state exists for less time than the chosen cadence, the test can skip it and report a misleading failure or pass. A fast, bounded poll can be justified when the callback is cheap, the state is transient, and the evidence shows the execution environment can carry the load.

Avoid polling a broad assertion until it passes. Repeatedly running an entire page audit can turn genuine intermediate defects into invisible noise. Wait for one readiness signal, then make the detailed assertions once. If the detailed assertion fails, keep that failure instead of treating it as another reason to sleep.

Do not share one FluentWait instance across test threads. The Java API makes no thread-safety guarantee for FluentWait. Give each wait a clear input and owner. Parallel tests may use equivalent configuration, but they should not coordinate through a mutable wait object.

A product-side completion event can be better than frequent DOM observation when the event is reliable and available through an interface the test already owns. That choice costs integration complexity and can create a second source of truth if the UI does not agree with the event. The final assertion should still verify the user-visible outcome.

Skip CPU optimization when the evidence shows no CPU problem. If the client, driver, browser, and Grid remain healthy while a server job exceeds its supported completion time, file the product or environment issue with the correlated job evidence. Changing polling may reduce diagnostic traffic, but it should not redefine the expected product behavior.

Finally, resist a universal number in code review. Ask what the condition calls, how long those calls take, how quickly the product state changes, how many sessions run together, and which component showed pressure. Those answers support a cadence decision. The adjective "fast" does not.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Does a shorter FluentWait polling interval always use more CPU?

Not automatically. The wait sleeps between sequential evaluations, while the callback may spend most of its time blocked on remote WebDriver calls. Measure the client JVM, browser or Grid, poll count, and callback duration before naming CPU as the bottleneck.

How often does FluentWait evaluate the condition?

Selenium evaluates the condition, checks the timeout after an unsuccessful evaluation, and then sleeps for the configured interval before trying again. Condition execution time adds to the start-to-start gap, so pollingEvery is not a fixed-rate scheduler.

Why does my explicit wait run longer than its configured timeout?

An implicit wait or another slow command inside the callback can extend each evaluation. Selenium warns that mixing implicit and explicit waits can produce unpredictable elapsed times, and the callback cannot be interrupted halfway through a WebDriver command by the FluentWait timeout.

What should I log when a Selenium wait overloads CI?

Start with each poll's start time, callback duration, result, and exception class. Add decorated WebDriver and WebElement method counts, process-level CPU observations, session identity, and Grid or browser evidence so the team can locate where the work occurs.

Should every wait in a Selenium suite share one polling interval?

No. A cheap readiness check and a multi-command table scan have different costs and transition speeds. Keep a small set of reviewed cadence profiles, but let evidence from the condition and execution environment decide which profile applies.