Why I Keep the Old Path Until the New One Proves Itself

Replacing a scheduled path is deceptively simple. The old job runs on a clock, the new worker reacts to a richer trigger, and both appear to perform the same task. It’s tempting to disable the old entry, enable the new one, and call the migration complete when the first run succeeds.

a rack of servers in a server room

Replacing a scheduled path is deceptively simple. The old job runs on a clock, the new worker reacts to a richer trigger, and both appear to perform the same task. It’s tempting to disable the old entry, enable the new one, and call the migration complete when the first run succeeds.

I don’t do that anymore. A scheduler carries behavior that isn’t visible in the task’s main function. It decides what “due” means, what happens after downtime, whether overlapping runs are allowed, and how a missed interval returns to the queue. A new path can execute the work correctly while changing all four of those semantics.

So I keep the old path intact while I learn whether the new one understands time the same way.

The first step is to make the due set visible. For a bounded period, both paths calculate which items they believe are eligible, but only the old path retains execution authority. The new worker records its proposed set without acting. I compare identities and reasons, not just totals. Two schedulers can each report five due items while disagreeing about every item in the list.

Differences are expected at first. One path may interpret a boundary in local time while another uses UTC. One may round timestamps; another may compare them exactly. A new worker may regard an item as due immediately after a restart, while the old job waits for its next interval. None of these are implementation trivia. They determine whether work is skipped or repeated.

I also compare the quiet periods. It’s easy to study what each path selects and ignore what each path correctly leaves alone. A migration can look successful during a busy sample because there is always work to find. The empty interval shows whether the new scheduler invents catch-up work, keeps reconsidering completed items, or treats the absence of a result as permission to try again.

The old configuration stays versioned and runnable, but I don’t leave two active writers racing for the same task. “Keeping the old path” doesn’t mean duplicating side effects. During comparison, the new path observes. At cutover, authority moves explicitly. The old schedule is disabled in a way I can inspect, not deleted in a burst of confidence.

That distinction matters because rollback is temporal. If the new worker mishandles a due boundary, simply turning the old cron entry back on may not restore the prior behavior. Time has advanced. Items may have been claimed, partially processed, or marked for a future attempt. Before reactivating the old path, I reconcile what the new path considered and what it actually changed.

I give each execution a stable task identity derived from the work, not from whichever scheduler found it. Both paths can then refer to the same logical occurrence. The identity makes duplicates visible and lets the executor reject a second claim that arrives through a different route. Without it, the migration depends on the hope that the two schedulers won’t overlap at an awkward second. Computers are very good at finding awkward seconds.

Cutover begins with a clean boundary. I let the old scheduler finish its current claim, record the last occurrence it owned, and disable its next trigger. The new path reads the durable task state before it starts selecting. I don’t copy an in-memory queue or assume that an empty worker means empty work.

Then I watch full timing shapes rather than one happy invocation. The new path needs to cross an ordinary interval, an interval with no eligible work, and a restart. If the workflow has a legitimate catch-up rule, I exercise that rule with a safe test item. I want to see that one missed occurrence becomes the intended next action, not a burst of every theoretical tick since the process went away.

During that period, the old definition remains available as a comparison. When the new path surprises me, I can ask a concrete question: would the previous scheduler have selected this same occurrence under the same stored state? That’s more useful than arguing from memory about how cron “usually” behaves.

The old path also protects against a subtler migration error: changing task behavior and scheduling behavior together. I try to hold the task implementation constant while moving the trigger. If the new architecture requires a new payload or claim protocol, I stage that separately. Otherwise a missing result leaves me wondering whether the worker did the wrong work or never selected it correctly.

I don’t require the new scheduler to imitate every limitation. The point of replacing it may be to gain better concurrency control or clearer catch-up behavior. Those differences have to be deliberate and testable, though. “The new one is event-driven” isn’t a decision about what happens after six hours offline. The migration note states the new rule and the old rule so the behavior change can be reviewed on purpose.

Eventually the old path stops being useful evidence. Once the new scheduler has crossed the relevant boundaries, its execution identities reconcile with durable task state, and restart behavior is understood, I remove the disabled trigger. I retain the history and the previous configuration in normal version control, not as a secret alternate production route.

This approach is slower than celebrating the first successful run. It’s faster than discovering weeks later that success came from the easy part. The task was never the whole path. Time, eligibility, overlap, and missed work were part of the interface all along.

When I replace a scheduler now, I ask one final question before deleting the old entry: which clock behaviors has the new path actually crossed? If the answer is only “it ran once,” the old path stays disabled but available, and the new one still has proving to do.