Waits: Implicit, Explicit & Fluent

If there is one topic that separates a reliable automation suite from a flaky one, it's waits. A browser page is not instant — JavaScript runs, AJAX calls fire, spinners spin, and elements attach to the DOM asynchronously. Selenium, on the other hand, executes commands as fast as your code lets it. If you ask for an element before the page has actually produced it, you get a NoSuchElementException or a click that lands on nothing. This lesson covers the three ways Selenium lets you wait for the browser to catch up: implicit wait, explicit wait, and fluent wait.

Why Waits Matter: The RecipeBox Spinner Problem

On RecipeBox's recipe list page, the recipe cards don't exist in the HTML when the page first loads. A spinner shows while an AJAX call fetches the recipes, and only once that call resolves do the .recipe-card elements get injected into the DOM. If your test calls driver.findElements(By.className("recipe-card")) immediately after driver.get(...), you'll usually get an empty list — not because there are no recipes, but because you asked too early.

Note: This is precisely the class of bug that makes a test "flaky" — it passes on a fast machine or a warm cache, and fails on a slow CI runner. The fix is never Thread.sleep(); it's telling Selenium exactly what condition to wait for.

1. Implicit Wait

An implicit wait is a global setting on the WebDriver instance. Once set, every findElement() / findElements() call will poll the DOM for up to the given duration before throwing NoSuchElementException.

WebDriver driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

driver.get("https://recipebox.example.com/recipes");

// Selenium will silently retry finding this for up to 10 seconds
List<WebElement> cards = driver.findElements(By.className("recipe-card"));
Limitations of implicit wait:
  • It only helps with presence in the DOM — not visibility, not "clickable", not text content.
  • It applies globally to the driver, so you can't wait longer for one slow element without slowing down every other lookup too.
  • It says nothing about the spinner disappearing — the cards could exist in the DOM while still hidden behind a loading overlay.

2. Explicit Wait (WebDriverWait + ExpectedConditions)

An explicit wait targets one specific condition on one specific element, and it's the tool you should reach for almost every time. It's built from a WebDriverWait object and a condition from ExpectedConditions.

WebDriver driver = new ChromeDriver();
driver.get("https://recipebox.example.com/recipes");

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));

// 1. Wait for the loading spinner to disappear
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loadingSpinner")));

// 2. Then wait for at least one recipe card to actually be visible
WebElement firstCard = wait.until(
    ExpectedConditions.visibilityOfElementLocated(By.className("recipe-card"))
);

// 3. Assert once we know the list has really finished loading
List<WebElement> cards = driver.findElements(By.className("recipe-card"));
Assert.assertTrue(cards.size() > 0, "Expected at least one recipe card to load");

Common ExpectedConditions you'll use constantly:

ConditionWhen to use it
visibilityOf(element)Element exists and is displayed — e.g. a recipe card after the spinner clears
elementToBeClickable(locator)Element is visible and enabled — e.g. the "Save Recipe" button before clicking it
invisibilityOfElementLocated(locator)Waiting for a spinner/overlay to go away
textToBePresentInElement(el, text)Waiting for a status message, e.g. "Recipe saved!"
presenceOfElementLocated(locator)Element just needs to exist in the DOM (rare — usually you want visibility)

3. Fluent Wait

FluentWait is WebDriverWait's more configurable cousin. It lets you set a custom polling interval (how often Selenium re-checks the condition) and tell it which exceptions to swallow while it's still polling — useful when an element is being re-rendered and briefly throws StaleElementReferenceException mid-poll.

Wait<WebDriver> fluentWait = new FluentWait<>(driver)
        .withTimeout(Duration.ofSeconds(20))
        .pollingEvery(Duration.ofMillis(500))
        .ignoring(NoSuchElementException.class)
        .ignoring(StaleElementReferenceException.class);

WebElement firstCard = fluentWait.until(driver1 ->
    driver1.findElement(By.className("recipe-card"))
);
Note: Reach for FluentWait only when a plain WebDriverWait isn't enough — e.g. RecipeBox's list re-renders every 500ms while new recipes stream in, and you want to poll on that same cadence while ignoring the brief staleness exceptions that come with it. For most day-to-day waits, WebDriverWait is simpler and just as effective.

Never Mix Implicit and Explicit Waits

Selenium's own documentation warns against combining implicit and explicit waits on the same driver. When both are configured, the effective wait time becomes unpredictable — the implicit wait's polling can interact badly with an explicit wait's own polling, sometimes summing delays, sometimes causing a wait to appear to take far longer than either timeout alone. This is a well-known source of hard-to-diagnose flaky/slow tests.

RecommendationWhy
Prefer explicit waits everywhereTargets the exact condition you actually care about, on the exact element you care about
Avoid implicitlyWait() entirely in new frameworksGlobal, coarse, and dangerous when mixed with explicit waits
Never use Thread.sleep() as your primary wait strategyIt's either too short (still flaky) or too long (slow suite) — it never actually reacts to the app's real state

Worked Example: Waiting on RecipeBox's Recipe List

public class RecipeListWaitTest {

    private WebDriver driver;

    @BeforeMethod
    public void setUp() {
        driver = new ChromeDriver();
        driver.get("https://recipebox.example.com/recipes");
    }

    @Test
    public void recipeListLoadsAfterSpinner() {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));

        // Wait for the AJAX spinner to disappear
        wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loadingSpinner")));

        // Wait until at least one recipe card is visible before asserting
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("recipe-card")));

        List<WebElement> cards = driver.findElements(By.className("recipe-card"));
        Assert.assertTrue(cards.size() > 0, "Recipe list should not be empty after loading");
    }

    @AfterMethod
    public void tearDown() {
        if (driver != null) driver.quit();
    }
}
Next up: Now that your tests reliably wait for elements before touching them, move on to Actions Class to handle complex gestures Selenium's basic API can't — like hovering, drag-and-drop, and key combinations.