◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
Articles/Dev Tools
⬡ Dev Tools/2026-06-25Advanced

Why Your 9 AM Reminder Stops Arriving Abroad — Making Expo Local Notifications Survive Time Zones and DST

Daily reminders built with Rork (Expo) can drift to the wrong local time when users travel or DST flips. Here is the timeInterval trap, and a design that reschedules against local wall-clock time, with working code.

Rork577expo-notifications7Time ZonesLocal NotificationsReact Native238DST

✦ Premium Article

A user of one of my wallpaper apps once wrote in to say that the morning "image of the day" was arriving in the evening after they moved to Europe. It fired reliably at 9 AM in Japan, so my first guess was something server-side. The real cause sat much earlier in the stack: how the local notification trigger was built.

Running about six healing- and manifestation-style apps on my own, the bugs I dread most are the ones that never reproduce on my own device. A drifting daily reminder is the textbook example. This article separates out exactly why Expo local notifications drift across time zones and DST, and lays out a rescheduling design that keeps them anchored to local time, with code you can drop in.

Why "local 9 AM" stops arriving

The drift comes down to one distinction: does the trigger point at an absolute instant, or at a time on the device's local calendar? expo-notifications triggers fall into three families.

Trigger typeWhat it points atAcross travel / DST
timeInterval (n seconds)An absolute instant (fixed seconds from scheduling time)Drifts
date (a fixed Date)An absolute instant (one point in UTC)Drifts
daily / calendar (hour, minute)A time on the device's local calendarTracks correctly

The pattern that bites most is computing "seconds until the next 9 AM" and passing it to a timeInterval trigger. It looks right, but at scheduling time it is frozen into an absolute "fire N seconds from now." When the user moves nine hours east, that promised instant does not move. So it rings in the evening locally.

Passing a fixed Date drifts for the same reason. new Date(2026, 5, 26, 9, 0) is baked into one UTC instant using the device's current offset, so after a move it no longer lines up with the local clock.

I had shipped both. Because I wanted to vary the message per day, I had deliberately stacked one date at a time instead of using a calendar trigger. In exchange for per-day copy, I gave up time-zone resilience.

The basics: if you only need local time, use DAILY

If the same copy at the same local time every day is enough, the answer is simple. With SchedulableTriggerInputTypes.DAILY, the OS re-evaluates against the local calendar, so both travel and DST switches are absorbed for you.

import * as Notifications from 'expo-notifications';
 
export async function scheduleDailyReminder(hour: number, minute: number) {
  // Clear the existing daily slot first, then re-add (avoid duplicates)
  await Notifications.cancelAllScheduledNotificationsAsync();
 
  await Notifications.scheduleNotificationAsync({
    content: {
      title: 'Your image of the day is here',
      body: 'A quiet change of mood for your home screen.',
    },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.DAILY,
      hour,   // interpreted in the device's local time
      minute,
    },
  });
}

On iOS this maps to a UNCalendarNotificationTrigger; on Android, to a repeating alarm. Both mean "local hour:minute," so a reminder set in Tokyo still rings at the local 9 AM nine hours away. Swapping timeInterval for DAILY is, in fact, the most common one-line fix.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Why timeInterval/Date triggers drift across time zones and DST, and how the DAILY calendar trigger tracks local time instead
✦A design that stores the intended local hour:minute and rebuilds the schedule on foreground when a time-zone change is detected
✦Safe next-fire computation that handles the spring-forward gap (a 2:30 that does not exist) while keeping a rolling window under the 64-notification cap
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links