Engineering3 min read

Yesterday is harder than it looks

A daily job that scans 'yesterday' has to answer whose yesterday. Get it wrong and users in most of the world silently get the wrong day.

Noomachy DevTracker scans what you built yesterday and has the posts ready by the time you start work. Simple feature. The word "yesterday" does almost all of the damage.

Whose yesterday

The original code computed the day boundary from the browser's clock:

const start = new Date(date + "T00:00:00");
start.setHours(0, 0, 0, 0);

This has two problems. The obvious one is that a scheduled job has no browser, so the moment the feature moved from a button to a cron job the code had nothing to read.

The subtler one is that even in the browser it was wrong for anyone whose local day does not align with UTC, which is most of the planet. GitHub returns and filters commit timestamps in UTC. A user in Lagos who commits at 23:30 local time has committed at 22:30 UTC on the same date, but a user who commits at 00:30 local has committed at 23:30 UTC on the previous date. Treat UTC dates as local dates and that second commit lands on the wrong day, every time.

The correct shape

A local calendar date maps to a half open interval in UTC:

[start, end)

For 2026-09-18 in Africa/Lagos, that is 2026-09-17T23:00:00Z to 2026-09-18T23:00:00Z. Lagos is UTC+1, so the local day begins an hour before UTC midnight.

The important detail is how you compute end. The obvious approach is to add 24 hours to start. That is wrong twice a year.

Days are not 24 hours

On a daylight saving transition, a local day is 23 or 25 hours long. In New York on 2026-03-08 the clocks go forward and the day is 23 hours. On 2026-11-01 they go back and it is 25.

Adding 24 hours to the start of a 23 hour day includes an hour of the next day. Adding 24 to a 25 hour day drops an hour of commits on the floor. Neither produces an error. They produce quietly wrong output on two days a year, which is the worst kind of bug because nobody notices and nothing gets reported.

The fix is to derive the end bound from the next local midnight rather than by arithmetic:

return {
  startUtc: fromZonedTime(`${localDate}T00:00:00`, timezone),
  endUtc: fromZonedTime(`${nextLocalDate}T00:00:00`, timezone),
};

Now the interval is whatever length the day actually was.

The inclusive boundary

One more detail. Our interval is half open, so end is exclusive. GitHub's until parameter is inclusive.

Pass endUtc directly and a commit landing exactly on midnight is counted in both adjacent days. Pass endUtc - 1ms and it is counted once, on the correct side.

That is a one millisecond bug that would produce a duplicate post roughly whenever someone commits precisely at midnight, which is rare enough that you would never find it by using the product and common enough that it would eventually happen to someone.

What we test

This is a module where tests earn their keep, because every failure mode is silent:

  • Africa/Lagos, which is UTC+1 all year with no DST
  • America/New_York across both DST transitions, asserting 23 and 25 hour days explicitly
  • Asia/Kathmandu, which is UTC+5:45, because 45 minute offsets break code that assumes whole hours
  • Month, year and leap day boundaries
  • Adjacent days producing contiguous non overlapping intervals

And the one that matters most: a commit at 23:30 Lagos time belongs to that Lagos day, while a commit at 00:30 Lagos time does not belong to the previous one.

Scheduling on top of this

The daily job sweeps every fifteen minutes and asks, per workspace, whether the user's local wall clock time has just passed their configured hour.

Comparing wall clock time rather than a stored UTC offset is what makes a 07:30 schedule stay at 07:30 through a DST change without anything being rewritten. The alternative, storing "06:30 UTC" when the user picks 07:30 Lagos, works until the offset changes and then silently drifts by an hour.

The test for this walks a full simulated day at the sweep cadence and asserts the schedule fires exactly once. There is a second one that walks 26 hours across a DST transition and asserts the same thing, because "fires once" and "fires once even when the day is 25 hours long" are different claims.

engineeringtimezonesscheduling

Keep reading