How to Use Explicit and Fluent Waits in Selenium with C#
Use Selenium’s configurable C# waits to poll for real browser conditions, handle transient exceptions, and avoid unpredictable timing from mixed wait strategies.
Use WebDriverWait with an Until predicate to wait for a specific browser condition in Selenium for C#. Configure its timeout, polling interval, and ignored exceptions when needed. Selenium’s .NET WebDriverWait derives from DefaultWait<IWebDriver>, which provides the configurable behavior often called a fluent wait. Avoid mixing implicit and explicit waits because the combined timing can be unpredictable.
This guide uses predicates rather than built-in Expected Conditions: Selenium says .NET stopped supporting those in Selenium 4. The examples use the C# API’s documented TimeSpan arguments.
1. Add Selenium and create a driver
For a new .NET console project, add Selenium.WebDriver and Selenium.Support. The first package provides WebDriver; the support package provides WebDriverWait. A browser and compatible driver must also be available. Selenium Manager can help manage drivers in supported Selenium versions.
dotnet new console -n SeleniumWaits
cd SeleniumWaits
dotnet add package Selenium.WebDriver
dotnet add package Selenium.Support
The snippets below assume these imports and a valid IWebDriver instance:
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
using System;
IWebDriver driver = new ChromeDriver();
For a complete minimal run, put the following in Program.cs. It navigates to Selenium’s dynamic loading example, waits for the revealed element, and then reads its text. The example timeout is illustrative; choose one appropriate to your test environment.
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
using System;
IWebDriver driver = new ChromeDriver();
try
{
driver.Navigate().GoToUrl("https://www.selenium.dev/selenium/web/dynamic.html");
driver.FindElement(By.Id("reveal")).Click();
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
IWebElement revealed = wait.Until(d =>
{
var element = d.FindElement(By.Id("revealed"));
return element.Displayed ? element : null;
});
revealed.SendKeys("Displayed");
Console.WriteLine(revealed.GetAttribute("value"));
}
finally
{
driver.Quit();
}
2. Understand explicit and fluent waits
An explicit wait repeatedly evaluates a condition and continues when that condition succeeds or the timeout expires. The condition is specific to the point in the test where the wait appears: an element becomes visible, text appears, or a control becomes usable.
In Selenium’s .NET binding, use WebDriverWait and configure its properties and methods. Since it inherits from DefaultWait<IWebDriver>, it has the configurable wait behavior developers often mean by “fluent wait.” Do not copy Java’s FluentWait constructors or withTimeout syntax into C#.
| Approach | What it does | Good fit |
|---|---|---|
| Explicit wait | Polls a named condition at a particular point | Dynamic page states and controls |
| Configurable wait (“fluent” behavior) | Sets timeout, polling interval, ignored exceptions, and timeout message | Conditions that need tailored retry behavior |
| Implicit wait | Applies a global delay policy to element-location calls | Usually avoid when explicit waits are used |
| Fixed sleep | Always pauses for a chosen duration without checking state | Only when a genuine fixed pause is required |
3. Write explicit wait predicates
Until accepts a function that is retried until it returns a successful value or the wait times out. A boolean predicate can report a condition; a predicate returning an element can both check readiness and provide the element to the next line.
Wait until an element is displayed
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
IWebElement result = wait.Until(d =>
{
var element = d.FindElement(By.Id("result"));
return element.Displayed ? element : null;
});
If the element may not yet exist, the default wait behavior retries a missing-element lookup. Returning null or false means the condition is not ready; returning the element or true means success.
Wait for text
bool hasConfirmation = wait.Until(d =>
{
var element = d.FindElement(By.Id("status"));
return element.Displayed && element.Text.Contains("Saved", StringComparison.Ordinal);
});
Wait for an element to be enabled
IWebElement submit = wait.Until(d =>
{
var element = d.FindElement(By.CssSelector("button[type='submit']"));
return element.Displayed && element.Enabled ? element : null;
});
Displayed and enabled are useful readiness checks, but they do not guarantee that a click will succeed: overlays, animation, scrolling, or a stale element can still interfere. Perform the action and handle only the transient failure modes that are appropriate to retry.
4. Tune polling and ignored exceptions
The timeout is the total time allowed for the condition to succeed. PollingInterval controls how often the predicate is reevaluated. Selenium’s documented C# example sets a 300 ms interval and ignores ElementNotInteractableException for a retryable interaction.
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10))
{
PollingInterval = TimeSpan.FromMilliseconds(300),
Message = "The result did not become ready in time."
};
wait.IgnoreExceptionTypes(typeof(ElementNotInteractableException));
IWebElement result = wait.Until(d =>
{
var element = d.FindElement(By.Id("result"));
return element.Displayed ? element : null;
});
Use ignored exceptions narrowly. If an exception represents a permanent defect—such as an invalid selector—ignoring it can hide the real problem until a less informative timeout occurs. The predicate must only report success after the requested condition or action actually succeeded.
For an action that may transiently be non-interactable, place the action inside the predicate and return success only after it completes:
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10))
{
PollingInterval = TimeSpan.FromMilliseconds(300)
};
wait.IgnoreExceptionTypes(typeof(ElementNotInteractableException));
bool typed = wait.Until(d =>
{
var field = d.FindElement(By.Id("revealed"));
if (!field.Displayed) return false;
field.SendKeys("ready");
return true;
});
Be careful with actions inside a retried predicate: if an action partially succeeds before throwing, retrying can duplicate its effect. Prefer waiting for readiness first, then performing non-idempotent actions once. Retry an action only when its failure semantics make that safe.
5. Choose a useful condition and timeout
Pick the condition that represents the next operation’s actual prerequisite. For a click, visibility and enabled state may be necessary but not sufficient; for a result, wait for the expected result text or state change. Avoid a generic “page ready” check when the test needs a particular application state.
- Use a timeout that reflects expected application and CI variability; the documentation’s two-second examples are demonstrations, not universal values.
- Keep polling intervals reasonable. Very frequent polling adds repeated WebDriver commands; very slow polling can notice readiness late.
- Set a useful custom
Messageso a timeout identifies the condition that failed. - Keep the predicate fast and focused. Avoid unrelated network or application work inside it.
A timeout bounds how long the test waits. It does not make a broken page ready or guarantee the operation will succeed.
6. Avoid mixing implicit and explicit waits
An implicit wait affects element-location calls throughout the session. An explicit wait also polls and may perform element lookups in its predicate. Combining the two can make each poll itself wait, so the total can exceed the apparent explicit timeout. Selenium documents an example where a 10-second implicit wait and a 15-second explicit wait can result in a timeout after 20 seconds.
Prefer an implicit wait of zero and explicit waits for the dynamic conditions that matter. If an existing suite sets a nonzero implicit wait, account for that global setting before diagnosing apparently overlong explicit waits.
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
WebDriverTimeoutException |
The condition never became true, locator is wrong, or timeout is too short for the environment | Check the locator and observed page state; make the predicate match the required state; adjust timeout only when the operation is legitimately slower. |
| Wait takes longer than its timeout | A nonzero implicit wait is nested in explicit-wait polling | Remove the competing implicit wait and use explicit conditions. |
| Predicate succeeds too early | It returns true without verifying the intended state or action result | Return success only after checking the actual state or completing the action. |
StaleElementReferenceException |
The page replaced or refreshed the element after it was located | Re-find the element inside the predicate. Ignore stale references only when replacement is an expected transient state. |
| Click still fails after waiting | Displayed and enabled do not rule out overlays, animation, or interception | Wait for the blocking state to clear or for the specific click precondition; inspect the failure before adding retries. |
| Code cannot find Expected Conditions in .NET | Selenium 4 .NET does not include built-in Expected Conditions support | Express the condition with an Until predicate. Add a separate library only if intentionally selected and verified for the project. |
| Compile error using Java wait syntax | Wait APIs differ by language binding | Use C# TimeSpan and .NET members shown here; check the API for the installed package version. |
| Exception appears only at timeout | A broad ignored-exception list is hiding a persistent failure | Narrow ignored exception types and report a useful timeout message. |
8. Performance, reliability, and maintenance
Explicit waits improve reliability by synchronizing on application state instead of guessing with fixed delays. They do not eliminate flakiness if the condition is too weak, the locator is unstable, or an action has side effects when retried.
- Command overhead: each poll can issue one or more WebDriver commands. Keep predicates small and avoid excessively short polling intervals across large test suites.
- Failure diagnosis: use clear timeout messages and capture relevant page state when a wait fails so the missing condition can be identified.
- Retry safety: conditions are naturally retryable; actions may not be. Separate “wait until ready” from “submit once” when repeating the action could duplicate work.
- Version compatibility: compile against the Selenium .NET packages actually installed. Do not infer a C# API migration from documentation for a different binding.
- Cost: Selenium waits have no per-wait service charge, but longer waits and repeated polling consume test runtime and browser/CI capacity.
9. ScreenshotNeo: capture a page without setting up a browser
If the goal is to capture a page rather than interact with it as part of a Selenium test, ScreenshotNeo is a website screenshot API and MCP server. It returns a screenshot or PDF from one GET request. Its clean-shot flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.
Or skip the browser setup
See the ScreenshotNeo API documentation. This cURL call saves a WebP screenshot:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie banners, popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, and failed loads are never billed.
- An MCP server lets AI agents, including Claude, Cursor, and other MCP clients, take screenshots.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Plans also include 15,000 for $15, 60,000 for $39, 250,000 for $99, and 1,000,000 for $249. Yearly billing gives two months free, and every feature is on every plan.
Sign up for 1,000 free screenshots a month, with no card required.
10. FAQ
Is FluentWait a separate class I should construct in C#?
Use Selenium .NET’s WebDriverWait, which inherits configurable wait behavior from DefaultWait<IWebDriver>. The term “fluent” describes that configuration style here.
Can I use Expected Conditions with Selenium 4 for .NET?
The Selenium guide says built-in Expected Conditions support stopped for .NET in Selenium 4. Use predicates with Until, or deliberately choose a separate dependency after verifying it.
Should every test use the same timeout?
Not necessarily. Choose timeouts based on the operation and execution environment, and keep them long enough for legitimate variability without masking a failed condition.


