Heuristic:Teamcapybara Capybara Async Waiting And Retry
| Knowledge Sources | |
|---|---|
| Domains | Testing, Synchronization, Optimization |
| Last Updated | 2026-02-12 06:00 GMT |
Overview
Tuning `default_max_wait_time` and `default_retry_interval` to balance test speed against flakiness in asynchronous web applications.
Description
Capybara's `synchronize` method implements a retry loop that catches specific exceptions (primarily `ElementNotFound` and driver-specific stale element errors) and re-executes the block until either it succeeds or the timeout expires. The two key configuration parameters are `default_max_wait_time` (default: 2 seconds) and `default_retry_interval` (default: 0.01 seconds). These defaults represent a balance between fast test execution and tolerance for asynchronous page updates. The retry interval of 10ms means Capybara checks roughly 100 times per second, while the 2-second timeout provides a generous window for most AJAX operations.
Usage
Apply this heuristic when you are experiencing flaky tests due to timing issues, or when you need to optimize test suite execution time. Increase `default_max_wait_time` for slow CI environments or applications with heavy AJAX. Decrease it for fast applications to speed up test failure detection. Override per-query with the `wait:` option for specific slow operations.
The Insight (Rule of Thumb)
- Action: Configure `Capybara.default_max_wait_time` based on your application's typical async response time. Use per-query `wait:` overrides for known slow operations.
- Value: Default is 2 seconds / 0.01 second retry interval. Slow apps may need 5-10 seconds. Fast apps can reduce to 1 second.
- Trade-off: Higher wait time = fewer flaky tests but slower failure detection. Lower wait time = faster failures but more flakiness.
- Key insight: The RackTest driver's `wait?` returns `false`, bypassing the retry loop entirely — this is why RackTest is faster but cannot handle async content.
- Override pattern: `find(:css, '#slow-element', wait: 10)` for individual slow operations without changing global timeout.
Reasoning
The synchronize loop in `Node::Base` is Capybara's primary defence against asynchronicity problems (as stated in the source code documentation). It works on the principle that transient failures (element not yet rendered, element briefly removed during re-render) will resolve themselves within a short time window. The 0.01-second retry interval keeps CPU usage low while providing rapid responsiveness. The automatic reload feature (`automatic_reload = true` by default) means that during retries, stale element references are automatically refreshed by re-querying the DOM.
For drivers that do not support async processes (like RackTest), the synchronize method skips the retry loop entirely. This design means the same test code works across both sync and async drivers, but behavior differs: with RackTest, failures are immediate; with Selenium, failures wait up to `default_max_wait_time`.
Code Evidence
Configuration defaults from `lib/capybara.rb:80-82`:
# - **default_max_wait_time** (Numeric = `2`) - The maximum number of seconds to wait for asynchronous processes to finish.
# - **default_normalize_ws** (Boolean = `false`) - Whether text predicates and matchers use normalize whitespace behavior.
# - **default_retry_interval** (Numeric = `0.01`) - The number of seconds to delay the next check in asynchronous processes.
Core synchronize loop from `lib/capybara/node/base.rb:76-102`:
def synchronize(seconds = nil, errors: nil)
return yield if session.synchronized
seconds = session_options.default_max_wait_time if [nil, true].include? seconds
interval = session_options.default_retry_interval
session.synchronized = true
timer = Capybara::Helpers.timer(expire_in: seconds)
begin
yield
rescue StandardError => e
session.raise_server_error!
raise e unless catch_error?(e, errors)
if driver.wait?
raise e if timer.expired?
sleep interval
reload if session_options.automatic_reload
else
old_base = @base
reload if session_options.automatic_reload
raise e if old_base == @base
end
retry
ensure
session.synchronized = false
end
end
Retryable error detection from `lib/capybara/node/base.rb:134-136`:
def catch_error?(error, errors = nil)
errors ||= (driver.invalid_element_errors + [Capybara::ElementNotFound])
errors.any? { |type| error.is_a?(type) }
end