research/publish-time-that-survives-daylight-saving
A 9:30 AM Publish Time That Survives Daylight Saving Changes
Engineering Note · November 3, 2026 · 5 minute read · Astro · JavaScript · Scheduling
A publish time that stays at 9:30 AM in New York cannot be a fixed hour in UTC, because New York moves between UTC-4 and UTC-5. The reliable approach is to try both candidate UTC hours and keep the one where the New York clock reads 09:30. We ran that function over all 730 days of 2026 and 2027, and it landed on 09:30 every time. A fixed 13:30 UTC would have read 8:30 AM on every winter day.
Everything below was run with Node.js 22.23.4. No packages were installed for the tests. The function under test is the one this site uses to decide when a research post goes live.
What problem are we solving?
This site is static: the pages are built ahead of time, and a post only appears after a rebuild. We want a post dated Tuesday to go live at 9:30 AM New York time on Tuesday, not at midnight UTC. Midnight UTC is 8 PM the evening before in New York during daylight time, and 7 PM in winter. That would publish tomorrow’s post tonight.
So the build needs a function that answers one question: for the date 2026-11-03, what instant is 9:30 AM in New York?
Why does a fixed UTC hour fail?
New York observes daylight saving time. In 2026 the clocks went back on Sunday, November 1. Before that date 9:30 AM was 13:30 UTC. After it, 9:30 AM is 14:30 UTC.
We checked what a hard-coded 13:30 UTC reads on a November weekday:
console.log(new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York', timeStyle: 'short', hourCycle: 'h23'
}).format(new Date('2026-11-03T13:30:00Z')));
Output:
08:30
An hour early, for about four months of the year. The same bug runs the other way in spring, when a fixed 14:30 UTC would publish at 10:30 AM.
What does the working function look like?
const ZONE = 'America/New_York';
const HOUR = 9;
const MINUTE = 30;
const clock = new Intl.DateTimeFormat('en-US', {
timeZone: ZONE, hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
});
export function publishInstant(ymd) {
// New York is UTC-4 or UTC-5, so 9:30 there is 13:30 or 14:30 UTC.
for (const utcHour of [HOUR + 4, HOUR + 5]) {
const t = new Date(`${ymd}T${String(utcHour).padStart(2, '0')}:${String(MINUTE).padStart(2, '0')}:00Z`);
if (clock.format(t) === `${String(HOUR).padStart(2, '0')}:${String(MINUTE).padStart(2, '0')}`) return t;
}
throw new Error(`Could not place ${ymd} 09:30 in ${ZONE}`);
}
export function isPublishedAt(date, now = Date.now()) {
const ymd = typeof date === 'string' ? date : date.toISOString().slice(0, 10);
return publishInstant(ymd).valueOf() <= now;
}
The idea is simple. There are only two possible answers. Build both, ask Intl.DateTimeFormat what the New York clock reads at each, and keep the one that reads 09:30. We never do offset arithmetic ourselves, so we cannot get the rules wrong. The time zone database does that part.
The throw is deliberate. If neither candidate matches, something is wrong with the inputs, and a loud build failure is better than a post that quietly publishes at the wrong hour.
Does it land on 9:30 around the clock changes?
We printed the instant for dates on both sides of the 2026 and 2027 changes:
for (const d of ['2026-03-06','2026-03-09','2026-11-02','2026-11-03','2026-11-06','2026-11-09','2027-03-12','2027-03-15']) {
const t = publishInstant(d);
console.log(d, t.toISOString());
}
Output (with the New York clock reading appended by our test script):
2026-03-06 2026-03-06T14:30:00.000Z 09:30:00 EST
2026-03-09 2026-03-09T13:30:00.000Z 09:30:00 EDT
2026-11-02 2026-11-02T14:30:00.000Z 09:30:00 EST
2026-11-03 2026-11-03T14:30:00.000Z 09:30:00 EST
2026-11-06 2026-11-06T14:30:00.000Z 09:30:00 EST
2026-11-09 2026-11-09T14:30:00.000Z 09:30:00 EST
2027-03-12 2027-03-12T14:30:00.000Z 09:30:00 EST
2027-03-15 2027-03-15T13:30:00.000Z 09:30:00 EDT
The UTC hour moves from 14:30 to 13:30 between March 6 and March 9, 2026, and the local reading stays 09:30. The same holds in 2027.
What about every other day?
Spot checks are not proof, so we looped over every calendar day from January 1, 2026 through December 31, 2027 and compared the formatted New York time to 09:30:
days checked 730 wrong 0
Seven hundred thirty days, zero wrong. That includes all four clock-change weekends in the range.
Is the boundary exact?
A scheduler is only useful if the edge is crisp. We asked isPublishedAt about 2026-11-03 one second before the instant, and exactly at it:
const i = publishInstant('2026-11-03').valueOf();
console.log(isPublishedAt('2026-11-03', i - 1000), isPublishedAt('2026-11-03', i));
false true
One second before: hidden. At the instant: visible. The comparison is <=, so the post is live at 9:30:00 sharp.
How does the post actually appear?
The function only decides eligibility. A static build still has to run after 9:30 for the post to show up. On this site a scheduled GitHub Action calls a Cloudflare Pages deploy hook at 9:45 AM New York time, which rebuilds the site. Posts whose instant has passed are included in that build. Anything dated later stays out.
The rebuild trigger has the same daylight saving problem as the publish time. That is the subject of our companion note, a DST-safe weekday cron in GitHub Actions.
Limitations
- We tested the function, not a live deployment. We did not run a production rebuild for this note.
- It handles one zone and one time of day. Other zones need their own candidate offsets. A zone with a 30-minute offset change would need a different candidate list.
- It places a time of day on a date. It does not handle a time that falls inside the skipped hour on a clock-change night. New York changes clocks at 2 AM, so 9:30 AM never lands there.
- The test relies on the time zone data bundled with Node.js 22.23.4. A government that changes its rules needs a Node.js update to pick that up.
Related reading
- A DST-safe weekday cron in GitHub Actions
- Typed content collections in Astro for a research section
- Structuring an Astro static site with Pages Functions
Frequently asked questions
Why not just publish at 13:30 UTC?
New York is UTC-4 in summer and UTC-5 in winter. A fixed 13:30 UTC is 9:30 AM in summer and 8:30 AM after the clocks change in November. In our test, 13:30 UTC on November 3, 2026 read 8:30 AM in New York.
Does this need a date library such as Luxon or date-fns-tz?
Not for this job. Node.js ships with Intl.DateTimeFormat, which knows the time zone rules. We only ask it what the local clock reads at two candidate UTC instants and keep the one that says 09:30.
What happens on the two days a year when the clocks change?
Clocks change on Sunday mornings, and the function only needs to place 9:30 AM on a given date. On a weekday research site this never lands in the skipped or repeated hour. The test covered every calendar day of 2026 and 2027, including all four change dates.
How does a static site hide a post until its publish time?
The build compares the publish instant to the current time and leaves out posts that are still in the future. A static site only changes when it is rebuilt, so a scheduled rebuild shortly after the publish time is what makes the post appear.
Can this approach handle other time zones?
Yes, if you change the zone name and the list of candidate offsets. The pattern is to try each possible UTC offset for that zone and keep the one where the local clock reads the target time.