Clocks and Pacing
This page shows the clock settings of a run, described in Time and Pacing: the logical start time, the time zone, the factor, going live, the lag and strict pacing.
What It Is
A run's records carry dates and times, as the events of a real shop would. The logical start time and the time zone set those dates. The factor sets how fast the run goes against the wall clock: a backfill of last week runs without waiting, and a live demo runs in step with the wall clock. Going live switches from the first to the second within one run.
How to Use It
Logical Start Time
logical_start_time is the date and time that simulation time 0 maps to. Without it, the run uses the moment the environment is created. A run with a backdated start writes history:
logical_start_time under simulation takes now, a signed duration or an ISO datetime. This blueprint starts a day before the file is loaded:
DynamicRealtimeEnvironment(logical_start_time=...) takes the same datetime.
A datetime without an offset is read in the run's time zone, described next. In a blueprint it takes now, a signed duration such as -7d, or an ISO datetime, as YAML Blueprints describes.
Time Zone
timezone names the zone that record timestamps are written in, as an IANA name such as Australia/Melbourne. Every timestamp carries the offset of its moment, such as 2026-10-03T12:00:00.000+10:00, or +00:00 in UTC. By default the zone is that of logical_start_time when it has one, and UTC otherwise. A logical_start_time or go_live_at without an offset, now, and the default start are read in that zone:
The clock runs in UTC, so a daylight-saving change in the zone never adds or drops simulation time. In Melbourne, the clocks went forward an hour on 4 October 2026. A run of 24 simulation hours from 12:00 on 3 October therefore ends at 13:00 on 4 October, 2026-10-04T13:00:00.000+11:00. A replayed timestamp without an offset is read in the zone too.
Factor
The wall-clock duration of a stretch of the run is its simulation duration times factor:
factor under simulation, here read from an environment variable:
SimulationContext(sim_id="Shop", factor=1.0) takes the same number.
factor: 1.0follows the wall clock: a process that waits 5 simulation seconds waits 5 real seconds. It suits a live twin feeding a dashboard.factor: 0does not wait at all: the run goes as fast as the machine allows. It suits history, tests and experiments. Atfactor: 0each event is processed by SimPy's ownEnvironment.step, without the real-time arithmetic.strictleft unset is false here, because there is no wall clock to fall behind. A pendinggo_live_atis still checked before each event, and pacing starts at it.
Go Live
factor applies until the logical clock reaches go_live_at, and from that instant the run is paced at one simulation second per real second. With a backdated logical_start_time and factor: 0, one run writes the history as fast as it can, then keeps going in real time:
SimulationContext(factor=0.0, logical_start_time=..., go_live_at=...) takes the same two times as datetimes.
A sink with when: history receives the records stamped before go_live_at, and one with when: live the rest, so the two halves can go to different places:

Why one run rather than two. A demo of a data lake beside a live stream usually needs two datasets: a backdated history sitting in a lake, and a live stream arriving now. They have to be the same shop, with the same packers, the same distributions and the same task ids, or the join between them is meaningless.
Producing them needs two values of factor that conflict. History needs factor=0.0, so that a week of events is generated as fast as the machine allows. A live tail needs factor=1.0, so that events arrive at one simulated second per real second.
Without go_live_at, the only way to get both is to start two processes with the same random_seed and the same logical_start_time, one fast-forwarding into Parquet and one pacing into Kafka. That costs:
- A second full execution of the same simulation, so the history is generated twice.
- The seed and the start instant are written in two places, which have to be edited together. If one changes and the other does not, the two halves stop being the same shop, with no error.
- Nothing makes the two halves meet. Each process decides on its own where its half ends, so the two halves can overlap or leave a gap.
go_live_at replaces both processes with one. The run generates history unpaced up to that logical instant, then switches to real-time pacing and keeps going. There is one seed, one start instant and one instant where history ends and the live tail begins.
Three settings. Three separate settings make up the feature, and each answers a different question.
| Setting | Question it answers |
|---|---|
logical_start_time |
Where does the logical clock start? Set it in the past to backdate the history. |
go_live_at |
At which logical instant does pacing stop being factor and become real time? |
when on a sink |
Which sink receives each record? |
They are independent. go_live_at changes pacing only. It never routes a record. when routes records only. It never changes pacing. Used together, they give the history and the live stream from one run. In a blueprint, when: history and when: live compare each record's timestamp with go_live_at themselves. A Python predicate has to compare with the same instant. Otherwise the records switch sinks at a different instant from the one where the pacing switches.
On a DynamicRealtimeEnvironment, env.factor reports the run's factor, such as 0.0, until the logical clock reaches go_live_at, and 1.0 afterwards.
Parquet History, Kafka Live builds such a run.
Lag
At a factor above 0 the run follows the wall clock, and system.simulation.lag_seconds says how many seconds it is behind its pace, as telemetry. A run that keeps its pace reads about 0 at any factor, and the reading grows when the run falls behind. lag_interval sets how often it is published, in simulation seconds or a duration. By default, it is published every simulation second at a factor above 0. At factor: 0 it is off by default, because such a run has no pace to fall behind, and a lag_interval that turns it on reads 0. A run at factor: 0 that goes live starts publishing it at the go-live instant. A lag_interval of 0 turns it off:
Strict Pacing
A paced step that starts more than factor real seconds late has fallen behind the wall clock. With strict on, the run raises RuntimeError (Simulation too slow for real time (1.500s).), as SimPy's RealtimeEnvironment does. With strict off, the run goes on and system.simulation.lag_seconds reports the delay. A run that leaves strict unset gets the same default in every API:
- Paced run:
strictis true at afactorabove 0, and when the run has ago_live_at. - Unpaced run:
strictis false atfactor: 0with nogo_live_at, because an unpaced run has no wall clock to fall behind.
SimulationContext(strict=None) and DynamicRealtimeEnvironment(strict=None) are the defaults, and simulation.strict left out of a blueprint means the same. Write strict: false for a live run with a sink that can block, so the run goes on when the sink is slow. An explicit strict: true is checked at factor: 0 with no go_live_at too, and raises at the first step after the wall clock has moved, as SimPy's check does.
Before go_live_at, strict is never checked, whatever its value. After a pause, SimPy's env.sync() moves only the wall-clock anchor to now. The next event would then wait factor seconds for each simulation second since env_start. env_start is the start of the run, the go-live instant once the run has gone live, or the last resume_pacing(). env.resume_pacing() moves both anchors, so the next event waits factor seconds for each simulation second after now:
A context from SimulationContext.from_yaml runs in segments as the builder does, and resumes pacing with app.env.resume_pacing() in the same way.
The builder's environment is app.env. Call app.env.resume_pacing() before the next advance() of a run in segments, as Run in Segments describes.
| docs/snippets/pages/pacing.py:resume-pacing | |
|---|---|
How It Works
Pacing and the switch. Before each event, the environment checks the logical clock. At the first event at or after go_live_at, it sets the factor to 1.0 and moves its two anchors, env_start and real_start, so that the go-live instant counts as now. Without this move, the first paced event would wait on the wall clock for the whole backfill. The switch happens once, and always sets the factor to 1.0. A go_live_at at or before logical_start_time paces the whole run, with a warning. A run that ends before its go_live_at stays unpaced. A go_live_at or logical_start_time without an offset is read in the run's time zone, so the two can be compared whichever way each is written.
Stability and Limits
The limits of this page are listed with those of the whole section, in Stability and Limits on the Time and Pacing page.