A minimal, async-safe wrapper around DateTime.utc_now/0 that lets tests
freeze and travel time in the calling process.
Replace direct calls to DateTime.utc_now/0 with FrozenClock.utc_now/0 in
production code. In tests, freeze/0, freeze/1, travel/1, and travel/2
pin or shift the clock for the current process only.
Isolation
All state lives in the process dictionary under a single key, so freezing time
in one process never affects another. This makes async: true tests safe out
of the box.
Spawned processes see real time
Freezing affects only the calling process. Code that spawns processes
(Task.async/1, sending to a GenServer, Phoenix channels, ...) will see
the real system time in those children. If you need cross-process control,
use klotho.
Summary
Functions
Freezes time for the calling process.
Returns true when the calling process has frozen time.
Sets the frozen time to target.
Shifts the frozen time by amount of unit.
Removes any freeze for the calling process, restoring real time.
Returns the current time.
Functions
@spec freeze() :: :ok
Freezes time for the calling process.
With no argument, freezes at the current real time. With a DateTime, freezes
at that instant.
@spec freeze(DateTime.t()) :: :ok
@spec frozen?() :: boolean()
Returns true when the calling process has frozen time.
@spec travel(DateTime.t()) :: :ok
Sets the frozen time to target.
If the process is not already frozen, this freezes it at target.
@spec travel(integer(), :day | :hour | :minute | System.time_unit()) :: :ok
Shifts the frozen time by amount of unit.
Raises if the calling process has not frozen time first.
Examples
iex> FrozenClock.freeze(~U[2026-01-01 00:00:00Z])
iex> FrozenClock.travel(1, :hour)
iex> FrozenClock.utc_now()
~U[2026-01-01 01:00:00Z]
@spec unfreeze() :: :ok
Removes any freeze for the calling process, restoring real time.
Safe to call when time is not frozen.
@spec utc_now() :: DateTime.t()
Returns the current time.
When the calling process has frozen time, returns the frozen value; otherwise
delegates to DateTime.utc_now/0.
Examples
iex> FrozenClock.freeze(~U[2026-01-01 00:00:00Z])
iex> FrozenClock.utc_now()
~U[2026-01-01 00:00:00Z]