Skip to content
Koltrix

Scheduling emails across time zones in your app

Send at 9 a.m. local time, not 9 a.m. server time. Store zones correctly, handle daylight saving and design a scheduler that survives retries and outages.

Koltrix Team4 min read
Black and white analog alarm clock
Photo by insung yoon on Unsplash
On this page(9 sections)
  1. Decide what kind of time you mean
  2. Store the zone, not the offset
  3. Compute the send instant at the last moment
  4. Daylight saving: the two bad days
  5. Respect quiet hours
  6. A scheduler that survives real life
  7. Spread the load
  8. Test the awkward cases
  9. Key takeaways

"Send it at nine in the morning" sounds like one instruction. It is really three questions: whose nine, which day, and what happens if the system is down at nine.

Teams that send a trial reminder at 9:00 server time discover that half their users get it in the middle of the night. Teams that store a plain offset such as "+02:00" discover that clocks change twice a year. This guide covers how to schedule email so it arrives when people expect it, and keeps working through retries, outages and daylight saving.

Decide what kind of time you mean

Every scheduled email falls into one of two groups.

  • An instant. One moment in time, the same everywhere: "your trial ends at 2026-11-03 17:00 UTC". Store it in UTC.
  • A local wall-clock time. "Tomorrow at 9:00 where the user lives." The moment depends on the person's zone, and it moves with daylight saving.

Most lifecycle and reminder emails are the second kind. A common mistake is to convert a wall-clock time to UTC once, at signup, and store only the result. That works until the person travels or the clocks change.

Store the zone, not the offset

An offset such as +05:30 tells you the difference from UTC now. It does not tell you what it will be in March. A time zone name from the IANA database, such as Asia/Kolkata or America/New_York, carries the rules.

  • Store the IANA zone name on the user or account.
  • Get it from the browser (Intl.DateTimeFormat().resolvedOptions().timeZone) at signup, and let people change it in settings.
  • Keep a fallback for missing values, such as the account's country default or UTC, and use it consistently.
  • Do not guess the zone from the IP address when you can ask. An IP guess is fine as a suggestion, not as a record.

Use your language's time library, not hand-written arithmetic. Adding "24 hours" is not the same as "the same time tomorrow" across a daylight saving change.

Compute the send instant at the last moment

Keep the intent ("9:00 local, two days after signup") separate from the computed instant, and calculate the instant when you queue the job, not when the user signs up.

  1. Take the local date and wall-clock time you want.
  2. Combine them with the user's zone using a real time library.
  3. Convert the result to UTC and store that as the job's send_at.

If a user changes zone before the job runs, recompute pending jobs. If you cannot, at least recompute when the job is about to be sent.

Daylight saving: the two bad days

Twice a year the local clock does something strange.

  • Spring forward: a local time such as 02:30 does not exist. Your library will either shift it forward or throw an error. Decide on a rule, usually "send at the next valid time".
  • Fall back: a time such as 01:30 happens twice. Pick the first occurrence and write that in your design notes.

For 9:00 a.m. emails you will rarely hit these, because few zones change clocks at that hour. For jobs at 2 a.m. you will hit them every year. Test both days with fixed dates in your test suite.

Respect quiet hours

Sending at the right local time is half the job. The other half is not sending at the wrong one.

  • Set a window, such as 08:00 to 20:00 local, for anything non-urgent. If the computed time falls outside it, move it to the next window.
  • Separate transactional mail (password reset, receipt, security alert), which goes out now, from reminders and lifecycle mail, which can wait. See transactional and marketing types.
  • Honour weekends and holidays only where it matters for your product. Do not guess; make it a setting if people ask.

A scheduler that survives real life

Scheduling is a queue problem as much as a time problem. For the architecture of the queue itself, see an async email queue and lifecycle emails from your job queue.

Rules that keep it correct:

  • Store UTC instants and a status. A job is pending, sending, sent or failed, with the time it moved.
  • Poll for due work, do not rely on a timer per job. A worker that wakes every minute and takes jobs where send_at <= now and the status is pending recovers from restarts on its own.
  • Claim jobs safely. Use a database lock or an atomic update so two workers cannot send the same email.
  • Make the send idempotent. If the worker crashes after the provider accepted the message but before it recorded success, a retry must not send twice. See idempotency keys for email sends.
  • Use backoff for failures. See outbound retry and backoff.
  • Handle lateness. If the system was down for six hours, decide per email type whether a late message is still useful or should be dropped. A "your trial ends tomorrow" email an hour late is fine; the same email after the trial ended is not.

Spread the load

Everyone wants 9:00. A large account base all scheduled for the top of the hour creates a spike that strains your queue and your sending provider's rate limits. Add small random jitter, a few minutes either way, and stagger batches. Planning for email volume spikes covers limits and bursts.

Test the awkward cases

Write tests with fixed clocks for:

  • a user in a zone with a half-hour offset;
  • a send time on each daylight saving change day;
  • a user who changes zone between scheduling and sending;
  • a worker that restarts mid-run;
  • a job that is claimed twice.

Time bugs are cheap to find with a fixed clock and expensive to find in production.

Key takeaways

  • Decide whether each email is an instant or a local wall-clock time.
  • Store the IANA zone name, not an offset, and compute the send instant late.
  • Plan for both daylight saving days and for quiet hours.
  • Build the scheduler on a safe queue: UTC instants, atomic claims, idempotent sends and backoff.
  • Add jitter so everyone's 9 a.m. is not the same second.

Start with Koltrix

Your domain, one inbox, and an API that sends.

A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

SharePost on XLinkedIn