R4.02.3 · dive 3
Start-to-start cadence
The registry holds only the next run. When a job is produced the next timestamp is computed and the single score rewritten — start-to-start, anchored to the clock, not start-to-finish.
A scheduler keeps one entry per schedule, scored by the next run. Start-to-start measures the next run from this run's scheduled time plus the interval, so the cadence stays on the clock. Start-to-finish measures from when this run finished, so a slow run pushes every later run later. Set the run duration on the right and watch start-to-finish drift while start-to-start holds.
Three fires of an every-15-min schedule · start-to-start holds, start-to-finish drifts by the run duration
One entry, rewritten each cycle
The registry is a sorted set scored by next-run time. A schedule occupies one member. When the scheduler
produces the next job, it computes the next run from the cadence and ZADDs the new score
against the same member — rewriting the single entry, not appending a new one.
The registry's size is the number of distinct schedules, never the number of runs. A schedule that has fired ten thousand times still holds exactly one row. The question this dive answers is what the next run is computed from. Advance the fire count below: the same row's score is rewritten with the next run, and the registry stays at one entry.
One row, rewritten each fire · the score advances, the registry stays at one entry
The registry holds one entry per schedule, scored by the next run and rewritten each cycle; its size tracks schedules, not runs.
Start-to-start vs start-to-finish
Consider an every-15-minute schedule where a run takes 4 minutes. Start-to-start: the run scheduled for 09:00 produces the next at 09:15, the one after at 09:30 — the cadence is anchored to the clock and the 4-minute run does not move it. Start-to-finish: the 09:00 run finishes at 09:04, so the next is scheduled for 09:19, which finishes at 09:23, so the next is 09:38 — every run drifts later by the run duration, and after enough cycles the schedule has slid far off the clock.
Start-to-start keeps fifteen-minute marks at :00, :15, :30, :45; start-to-finish lets them creep. The interval scheduler computes the next millisecond from the previous scheduled millisecond plus the gap, never from a finish time.
- start-to-start
next = thisRun + interval— anchored to the scheduled time; the run duration does not move it.- start-to-finish
next = finish + interval— anchored to the finish; a slow run drifts every later run.- drift
- Per-fire slip of start-to-finish equals the run duration; it compounds across cycles.
Start-to-start anchors the next run to the scheduled time, so the cadence holds; start-to-finish anchors it to the finish, so a slow run drifts every later run.
The marker rings at the next run
Once the next run is known, the scheduler writes the wake marker so a blocked worker wakes at the right moment
rather than on a poll tick. The marker carries the next fire-time as its score —
ZADD markerKey nextTimestamp "1" — the same marker mechanism the blocking-vs-polling
module uses, here scored by the scheduler's next run. The worker parked on the marker wakes exactly when the next
scheduled run is due.
After computing the next run, the scheduler scores the wake marker by it, so the parked worker wakes at the next run rather than on a poll tick.
In EchoMQ — next-millis from the schedule
The interval path in addJobScheduler-11.lua computes the next run start-to-start and
rewrites the single registry entry.
-- addJobScheduler-11.lua (real) nextMillis = prevMillis + every -- start-to-start: from the prior SCHEDULED time if nextMillis < now then nextMillis = math.floor(now / every) * every + every -- snap forward to the next slot end -- storeJobScheduler rewrites the one registry entry ZADD repeatKey nextMillis schedulerId ZADD markerKey nextTimestamp "1" -- wake the worker at the next run
prevMillis + every is the start-to-start step; the snap-forward handles a missed slot
without abandoning the clock anchor. The next run is derived from the schedule, never from how long a run took.
The full scheduler-vs-delayed reconciliation across a worker pool — how a missed run, an overlapping run,
and a paused queue interact — is the scheduler subsystem the EchoMQ course covers.
A textbook delayed queue scores one run and is done; a scheduler rewrites its single score each cycle, and how it computes the next score governs whether the cadence holds. Start-to-start anchors to the clock; the registry stays one entry; the marker rings at the next run.
addJobScheduler-11.lua computes nextMillis = prevMillis + every
(start-to-start), snaps forward on a missed slot, rewrites the :repeat entry with
ZADD repeatKey nextMillis schedulerId, and scores the wake marker with
ZADD markerKey nextTimestamp "1".
The pattern says compute the next run from the schedule so the cadence holds; in EchoMQ the step is
prevMillis + every and the rewrite is one ZADD on the
:repeat entry, with a second on the marker.
References
The command that rewrites the single registry score, the probe that reads the prior scheduled time, the sorted set the schedule lives on, and the queue protocol whose job-scheduler cadence EchoMQ ports.
Sources
- Redis — ZADD — rewriting the single registry score with the start-to-start next-run time.
- Redis — ZSCORE — reading the prior scheduled time the next run is measured from.
- Redis — Sorted sets — the one-entry-per-schedule registry rewritten each cycle.
- BullMQ — the queue protocol — the job-scheduler cadence path EchoMQ ports.
Related in this course
- R4.02 · Schedulers & repeatable jobs — the module hub.
- R4.02.1 · Cron vs interval — the cadence the next run is computed from.
- R4.02.2 · The upsert, no duplicates — one entry per scheduler key.
- R4 · Score as meaning — the
:repeatZSET in the orientation arc. - E6 · Lifecycle controls — the EchoMQ course: the scheduler subsystem in depth.