All posts
5 min read

Dates and Time Zones in Apps: How Not to Get Them Wrong

Reminders sent an hour late, bookings on the wrong day, 'yesterday' that's actually today. Why dates are hard, the golden rule (store UTC, display local), ISO 8601, time zones vs offsets, daylight saving traps, and how to check your AI-built app handles them.

getting starteddatabasesdebuggingbeginner

Dates look simple and aren't. Almost every app eventually gets a bug report like: "My reminder came an hour late," "The booking shows the wrong day," or "Everything broke on the night the clocks changed." The good news is that a few rules prevent most of these.

Why dates are hard

  • The same moment is a different local time in different places: 9:00 in London is 10:00 in Paris and 04:00 in New York.
  • Daylight saving time shifts clocks by an hour, on different dates in different countries — and some places don't use it at all.
  • Time zone rules change, by government decision.
  • Your server, your database, and each user's device may all be in different time zones.
  • Some things are moments ("the payment happened at…") and some are calendar concepts ("my birthday," "every day at 9am") — and they need different handling.

The golden rule: store UTC, display local

UTC (Coordinated Universal Time) is the world's reference time. It has no daylight saving and never changes.

  1. Store moments in UTC. Every timestamp in your database — created, updated, paid, sent — in UTC.
  2. Convert to the user's local time only when displaying.

That way, the stored data is unambiguous, sorting and comparing work, and each user sees times in their own zone.

In PostgreSQL, use the timestamptz type ("timestamp with time zone") for moments. Despite the name, it stores a precise moment and converts for display. Plain timestamp has no zone information, and is a common source of hour-off bugs. (What Is a Database?.)

ISO 8601: the one format to use

When sending dates between systems — in APIs, JSON, logs — use ISO 8601:

2026-09-24T14:30:00Z          ← the Z means UTC
2026-09-24T16:30:00+02:00     ← same moment, with a +2 hour offset
2026-09-24                    ← a date with no time

It's unambiguous and sorts correctly as text. Never send 09/10/2026 — Americans read that as 10 September, most of the world as 9 October. (What Is JSON?.)

In JavaScript, new Date().toISOString() gives you ISO 8601 in UTC.

Time zones vs offsets

An offset like +02:00 is a fixed difference from UTC. A time zone like Europe/Paris is a rule set: +01:00 in winter, +02:00 in summer, with the switch on specific dates.

For anything in the future or recurring, store the time zone name, not the offset:

  • A user in Paris sets a daily reminder at 09:00. Store "09:00, Europe/Paris" — not "07:00 UTC," which will be wrong for half the year.
  • Time zone names come from the standard IANA time zone database: America/New_York, Asia/Tokyo, Australia/Sydney. Browsers can tell you the user's: Intl.DateTimeFormat().resolvedOptions().timeZone.

Moments vs calendar dates

Some dates aren't moments at all:

  • Birthdays, holidays, due dates — "24 September" is 24 September wherever you are. Store as a date only (PostgreSQL date), with no time and no zone. Storing a birthday as midnight UTC is how people's birthdays end up displayed a day early.
  • Opening hours, recurring schedules — "every weekday 09:00–17:00 local." Store the local time and the time zone.
  • Events that happened — payments, logins, messages. Store the moment in UTC.

The daylight saving traps

  • Some local times don't exist. When clocks spring forward, 02:30 is skipped in many zones. A reminder scheduled for then must go somewhere sensible.
  • Some local times happen twice. When clocks fall back, 01:30 occurs twice.
  • Not all days are 24 hours. Adding "24 hours" is not the same as adding "one day" across a daylight saving change.
  • Scheduled jobs on a server set to local time can run twice or not at all on changeover nights. Run servers and cron jobs in UTC. (Background Jobs and Cron.)

Use a library, and the platform

Date maths is a classic place for subtle bugs, especially in AI-generated code that hand-rolls conversions. Use well-tested tools:

  • Intl.DateTimeFormat (built into JavaScript) for displaying dates in the user's language and zone.
  • date-fns with its time zone add-on, or Luxon, for arithmetic and zone conversion.
  • The Temporal API — JavaScript's modern replacement for Date — handles zones, calendar dates, and durations properly, and is arriving in browsers and runtimes; check support for your targets before relying on it.
new Intl.DateTimeFormat("en-GB", {
  dateStyle: "medium",
  timeStyle: "short",
  timeZone: "Europe/Paris",
}).format(new Date("2026-09-24T14:30:00Z"))
// "24 Sept 2026, 16:30"

Checking your app

Ask your AI tool to audit date handling, and test yourself:

  • Database timestamps use timestamptz (or equivalent) and are stored in UTC
  • Birthdays and due dates are stored as dates, not timestamps
  • Recurring schedules store a time zone name, not an offset
  • APIs send ISO 8601
  • Dates are displayed in the user's time zone
  • Server and scheduled jobs run in UTC
  • Tests cover a daylight saving change and a user in a far-away zone

To test, change your computer's time zone to somewhere far away (like Pacific/Auckland) and use your app. Things that shift a day are bugs.


EasySpawn gives Claude Code a persistent workspace with a real Postgres database, so date handling can be tested against real timestamptz columns and real scheduled jobs — not assumptions. See how it works or join the waitlist.

Related: Design Your First Database · SQL for Beginners · Debugging for Beginners

Keep reading