Heuristic:Teamcapybara Capybara Frozen Time Detection
| Knowledge Sources | |
|---|---|
| Domains | Debugging, Testing, Synchronization |
| Last Updated | 2026-02-12 06:00 GMT |
Overview
Capybara uses monotonic clocks and detects frozen time to prevent infinite hangs caused by time-mocking libraries like `freeze_time`.
Description
Capybara's auto-waiting mechanism (the `synchronize` loop) relies on measuring elapsed wall-clock time to know when to stop retrying. If a time-mocking library (such as `timecop` in freeze mode or the `freeze_time` method) freezes `Time.now`, Capybara's timer never advances and the synchronize loop runs forever, causing tests to hang indefinitely. To defend against this, Capybara uses monotonic clock sources (`CLOCK_MONOTONIC_RAW`, `CLOCK_MONOTONIC_PRECISE`, `CLOCK_MONOTONIC`) which are immune to time mocking. On platforms where no monotonic clock is available, it falls back to `Time.now` and explicitly detects frozen time by checking if the timer start equals the current time, raising a `Capybara::FrozenInTime` error.
Usage
Apply this heuristic whenever you are writing tests that combine Capybara with time manipulation. If your test suite freezes time (for testing time-dependent behavior), you must use time travelling (moving the clock forward) rather than time freezing (stopping the clock). This is critical for avoiding test suite hangs.
The Insight (Rule of Thumb)
- Action: Never use time-freezing libraries (e.g., `freeze_time`, `Timecop.freeze`) in tests that exercise Capybara's waiting/retry logic.
- Value: Use `Timecop.travel` (time travelling) instead of `Timecop.freeze` (time freezing) when Capybara interactions are involved.
- Trade-off: Time travelling still advances the clock (less predictable timestamps) but keeps Capybara's synchronize loop functional.
- Symptom: Tests hang indefinitely with no error output — this is the hallmark of frozen time breaking Capybara.
- Error: If detected, Capybara raises `Capybara::FrozenInTime` with the message: "Time appears to be frozen. Capybara does not work with libraries which freeze time, consider using time travelling instead."
Reasoning
Capybara's `synchronize` method (in `Node::Base`) uses a `Timer` object that compares `monotonic_time` at start vs current. The timer's `expired?` method determines when to stop retrying element lookups. If time is frozen, the difference is always zero, so `expired?` never returns `true`, creating an infinite loop. The `stalled?` check catches the specific case where `@start == current` — meaning no time has passed at all — which is a strong signal that time is mocked.
The monotonic clock hierarchy (`CLOCK_MONOTONIC_RAW` > `CLOCK_MONOTONIC_PRECISE` > `CLOCK_MONOTONIC` > `Time.now`) ensures that on most modern Ruby platforms, time mocking does not affect Capybara at all. The `FrozenInTime` error only triggers on older platforms falling back to `Time.now`.
Code Evidence
Monotonic clock selection from `lib/capybara/helpers.rb:88-96`:
if defined?(Process::CLOCK_MONOTONIC_RAW)
def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC_RAW; end
elsif defined?(Process::CLOCK_MONOTONIC_PRECISE)
def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC_PRECISE; end
elsif defined?(Process::CLOCK_MONOTONIC)
def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC; end
else
def monotonic_time; Time.now.to_f; end
end
Frozen time detection from `lib/capybara/helpers.rb:108-118`:
def expired?
if stalled?
raise Capybara::FrozenInTime, 'Time appears to be frozen. Capybara does not work with libraries which freeze time, consider using time travelling instead'
end
current - @start >= @expire_in
end
def stalled?
@start == current
end
FrozenInTime exception class from `lib/capybara.rb:13`:
class FrozenInTime < CapybaraError; end