The Cron Cleanup That Broke Sends Twice, in Opposite Directions
Bakery Buddy's Automations engine sends real email on a baker's behalf: review requests, pickup reminders, birthday rebooking, that kind of thing. It's gated to send only between 9am and 7pm in the bakery's own local time, because nobody wants Buddy emailing their customer at 2am on their behalf. The cron that drives it runs once a day. Get that one clock time wrong and it isn't late. It just never runs.
That's exactly what happened, twice, nine days apart, for two completely different reasons.
One slot has to work for every timezone at once
The engine cron used to run at 13:00 UTC. On July 9, that got moved to 17:00 UTC on purpose, because 13:00 UTC is roughly 5 to 8am across the US depending on the zone, and every one of those times is before the 9am-7pm send window opens. 17:00 UTC is 10am Pacific and 1pm Eastern. It's the one hour of the day that's inside business hours everywhere in the US, in both daylight and standard time. Miss that slot and there's no such thing as "a little late." The cron runs once, checks the window, and if the window isn't open, the day's sends just don't happen.
The first tidy-up
Two days after that fix landed, a different commit collapsed every cron in the project to one shared time, "5am PST," for consistency. Automations went back to 13:00 UTC along with everything else.
Nothing crashed. Nothing errored. The cron ran every day, checked whether it was inside the bakery's 9am-7pm window, correctly found that it wasn't (13:00 UTC is 9am Eastern at the very edge, and earlier than that in every timezone west of it), and skipped. Every bakery outside Eastern time stopped auto-sending, silently, every single day.
Why "sent: 0" didn't raise a flag
The response the cron returned looked completely healthy: HTTP 200, some JSON, sent: 0. That's indistinguishable from "there was nothing to send today," which is a normal, boring outcome that happens plenty of times a week on a small account. The actual reason for the skip, that the window check had failed, never made it into the response at all. It was computed and then thrown away.
What finally surfaced it wasn't a monitor or an alert. It was a bakery owner noticing she wasn't getting any automation email and asking why. The investigation that followed found the cron-time bug sitting alongside two unrelated ones: a delivery-status column that recorded whether Resend had accepted a send, not whether it had actually delivered, so a bounced email still showed a green "Sent" badge; and a default sender address that was a spam-filter magnet for the baker's own product mail. Three independent things were each quietly reporting success. None of them were lying, exactly. They just weren't answering the question anyone actually needed answered.
The fix, landed on July 19, restored the 17:00 UTC schedule and, just as importantly, started reporting the skip reason in the cron's own response instead of swallowing it. From that point on, "nothing sent today" and "the window check failed" are two different, visible outcomes. It took 8 days for the first version of this bug to surface, and it surfaced by complaint, not by anything watching.
The next morning, the opposite mistake
Less than 24 hours after that fix shipped, a separate commit made a different, equally reasonable-sounding cleanup: move the daily crons to run overnight, Pacific time, so their results are sitting there ready when the bakery opens up each morning. That's a genuinely good idea for a job like the FreshBooks import or a low-stock check, work nobody needs to see happen live.
It also swept up invoice reminders, pickup reminders, and the cron that publishes scheduled social posts, into the same overnight slot: 10:00 UTC, 3am Pacific. Those three don't have a send-window guard the way the automations engine does. They fire the moment the cron runs, on whoever the cron is scheduled for. A real bakery's customers started getting payment reminders and pickup reminders timestamped 3am, and a scheduled Facebook post went out at 3am too.
This one didn't take 8 days to find. It was caught and fixed the same day, about two hours later, by moving those three crons back into the 10am-Pacific-to-1pm-Eastern daytime slot the automations engine already used.
Two edits, same instinct, opposite failure
Both commits were doing the same kind of thing: looking at a list of cron times that didn't share an obvious pattern and making them consistent. The first one made everything match by time of day. The second one made everything match by "internal jobs run overnight." Both are reasonable defaults if you're only looking at the schedule. Neither one asks the question that actually matters for a cron: who receives what this job produces?
A job whose output only a baker will ever see, an import, a stock check, can run at 3am and nobody notices or cares. A job whose output lands in a real customer's inbox, or gets posted to a bakery's public Facebook page, can't. That's not a property of the code inside the route. It's a property of the audience, and no amount of staring at the schedule file on its own tells you which crons in the list are which.
The schedule file is grouped by recipient now, not by time or by "what runs overnight." Every customer or public-facing send sits in one deliberate slot, the internal prep jobs sit in another, and there's a rule against moving anything across that line for tidiness. But the rule only exists because the same mistake had already been made twice, in both directions, inside of two weeks.
What generalizes past cron jobs
A scheduled job that quietly decides not to act is one of the easiest bugs to ship, because "did nothing" and "worked correctly and found nothing to do" produce the exact same response if you don't go out of your way to tell them apart. If any job in your system can legitimately skip its own work, make the skip a visible, named outcome, not a silent branch that returns the same 200 as success. The first version of this bug lived for 8 days because sent: 0 told nobody anything.
And when you're the one doing a "tidiness" pass on a list of configuration values, the question isn't whether the list looks consistent afterward. It's whether you know, for each entry, who actually receives the thing it produces, and whether the new time still works for them. A schedule that reads cleanly top to bottom and a schedule that's actually safe to run are two different properties, and only one of them shows up in a diff.
Frequently Asked Questions
What actually broke, twice?
Bakery Buddy's Automations engine sends real customer email, but only inside a 9am-7pm window in the bakery's own local timezone, and its cron runs once a day. A 'move every cron to one shared time' cleanup on July 11 pushed that cron outside the window for every US timezone except the very edge of Eastern, silently skipping every send west of it for 8 days. A second cleanup on July 20 moved invoice reminders, pickup reminders, and the social-post publisher into an overnight 3am Pacific slot meant for internal jobs, so real customers got reminder emails and a scheduled Facebook post timestamped 3am.
Why didn't the first bug get caught sooner than 8 days?
The cron's response was HTTP 200 with sent: 0, which is indistinguishable from a normal day with nothing to send. The actual reason for the skip, that the send-window check had failed, was computed and then dropped from the response entirely. It surfaced only because a bakery owner asked why she wasn't getting any automation email.
Why did the second bug get fixed the same day?
It was caught the same day it shipped and fixed about two hours later, largely because the team had just spent the prior week relearning that cron times aren't free to move for consistency. Real customers receiving 3am email is also a far louder, faster signal than a cron quietly returning sent: 0.
What's the actual rule now?
Group crons by who receives their output, not by what time looks tidy. Customer- and public-facing sends sit in one deliberate daytime slot that's inside business hours across every US timezone. Internal prep jobs with no external recipient run overnight. Moving a job across that line for the sake of a clean-looking schedule file is exactly the mistake that caused both failures.
I build Bakery Buddy for my wife Lindsay's cake studio, Marin Cake Studio, and this is the week that taught me a cron schedule isn't a formatting choice.
Ready to put this into practice?
Join the Waitlist