# How to Handle Time Zones and Dates in Your Product Without Bugs
> How to handle time zones and dates in a web app: store UTC instants and IANA zones, keep local times for future events, avoid DST bugs, test, 7-day plan.

```yaml
url: "https://makerhunt.io/studio/how-to-handle-time-zones-and-dates-in-your-product"
markdown: "https://makerhunt.io/studio/how-to-handle-time-zones-and-dates-in-your-product.md"
title: How to Handle Time Zones and Dates in Your Product Without Bugs
type: studio_article
category: Growth
tags: "time zones, time zones in web apps, store dates in UTC, daylight saving time bugs, IANA time zone, date handling best practices, JavaScript dates, SaaS development, makers, Growth"
featured_image: "https://txmhk1zrnc.ufs.sh/f/xSkWTCqmKWx94ZBZ7MKdYk9MQ6GatIoxndeHPK1Rm8U3jzq0"
published_at: "2026-10-28T09:55:39.633Z"
updated_at: "2026-10-28T09:55:40.241Z"
```

## About this studio article

How to handle time zones and dates in a web app: store UTC instants and IANA zones, keep local times for future events, avoid DST bugs, test, 7-day plan.

## Article

To handle time zones and dates in your product without bugs, store every moment that already happened as a UTC instant (a `timestamptz` column in Postgres), store each user's IANA time zone name such as `America/New_York` or `Asia/Kolkata` instead of a fixed offset, and keep future appointments as local time plus a zone, because time zone rules keep changing. Treat date-only values like birthdays and due dates as plain dates, not midnight UTC. Convert to the user's zone only for display, run "every day at 9" jobs on the user's clock, and test in a non-UTC zone across daylight saving dates.

This guide is for solo makers and small SaaS teams. The worked example is made up; the docs and rules are real, linked and checked in October 2026.

## Why do date bugs hurt a small product so much?

Most bugs annoy people. Date bugs make them doubt everything else your product says. Typical ones:

- **The reminder arrives an hour late,** or an hour early, the week the clocks change.
- **"Yesterday's" report is off by a day,** because "yesterday" was calculated in the server's zone, not the customer's.
- **A due date shows the day before** for everyone west of London, because a plain date was stored as midnight UTC.
- **An export says 03/04/2026,** and half your users read it as March and the other half as April.

None of these show up while you test from your own desk in June. They show up for a customer on the other side of the world, on a Sunday. Get the rules right once, while the codebase is small.

## What is the difference between an instant, a local time and a date?

Most time zone bugs in web apps come from mixing up three things that all look like "a date".

### An instant

One moment, the same everywhere: a signup, a payment, a login. It belongs to no zone.

### A local date and time (wall-clock time)

What a person reads off a clock: "my lesson is at 6 pm Thursday". It becomes an instant only once you attach a zone, and that instant can move if the zone's rules change. MDN's [Temporal reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal) gives the clearest example: a daily 8:00 am alarm should stay at 8:00 am whether or not it is daylight saving time, and whether or not you travelled.

### A date with no time

A birthday, a due date, an invoice date. 4 March is 4 March for everyone. Store it as "2026-03-04 00:00 UTC" and someone in New York sees 3 March.

Decide which of the three each field is. Most fixes below follow from that.

## How should you store dates in UTC without losing the user's time zone?

"Store dates in UTC" is good advice for instants and bad advice for the other two.

- **Instants go in a time zone aware column.** In Postgres that is `timestamp with time zone`, or `timestamptz` for short. The name misleads people: the [PostgreSQL date/time docs](https://www.postgresql.org/docs/current/datatype-datetime.html) say the value is converted to UTC on input and stored internally as UTC, and the original zone is not kept. On output it is converted to the session's `TimeZone` setting. It is a UTC instant, not "a time plus a zone".
- **Keep the user's zone as an IANA name in its own column.** Store `Europe/Berlin`, not `+01:00`. Postgres makes the point well: a full name like `America/New_York` carries daylight saving rules, while an abbreviation like `EST` is one fixed offset.
- **Future local events store local time plus zone.** For a lesson next April at 6 pm in Berlin, keep "2027-04-15 18:00" and "Europe/Berlin", and compute the instant when you need it. Convert today, and a rule change before April makes your stored instant wrong.
- **Plain dates go in a**`date`**column.**

The rules really do change. The [IANA Time Zone Database](https://www.iana.org/time-zones), which most operating systems, browsers and databases use, is updated whenever governments change offsets or daylight saving rules. Its 2026f release, dated 27 October 2026, notes that Cuba will not fall back on 1 November 2026 and stays on UTC-4 through 7 November 2027. Anyone who pre-computed UTC times for Cuban appointments next week holds wrong numbers.

For conversion in SQL, Postgres has `AT TIME ZONE` (see its [date/time functions page](https://www.postgresql.org/docs/current/functions-datetime.html)): `created_at AT TIME ZONE 'Asia/Kolkata'` gives Kolkata wall-clock time for a stored instant.

![Hands writing on a large paper desk calendar with a blue marker, a monitor and keyboard in the background](https://images.pexels.com/photos/6170644/pexels-photo-6170644.jpeg?auto=compress&cs=tinysrgb&w=1200&h=800&fit=crop)

## How do you find out which time zone a user is in?

The browser already knows. `Intl.DateTimeFormat().resolvedOptions().timeZone` returns the runtime's default zone as an IANA name, as described on MDN's [resolvedOptions() page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions).

Treat it as a guess, not a fact:

1. **Save it at signup** and show it in settings as a dropdown of IANA names with friendly labels.
2. **Ask once when it matters.** The first time someone schedules something, show "Times are in Berlin time (Europe/Berlin). Change". Travellers and people on work laptops set to head-office time will thank you.
3. **Notice when the browser disagrees with the saved zone** and offer to update it. Do not switch silently.
4. **Decide who owns the zone.** Team products usually need both a workspace zone (for "the weekly report goes out Monday at 9") and a personal zone for how each person sees times. Write down which one each feature uses.

Quiet hours and reminder timing depend on this too; the MakerHunt guide on [designing product notifications users don't turn off](https://makerhunt.io/studio/how-to-design-product-notifications-users-dont-turn-off) covers that side.

## How should you show dates and times to users?

Convert at the edge, in the user's zone, and show the zone when a wrong reading would cost someone.

- **Use**`Intl.DateTimeFormat`**with a**`timeZone`**option.** The [DateTimeFormat constructor docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) list it: any IANA name, and it defaults to the runtime's zone. Pass the user's saved zone, not the server's.
- **Show the zone on anything time sensitive.** The same docs describe a `timeZoneName` option with values such as `"short"` (PST or GMT-8) and `"shortOffset"` (GMT-8).
- **Use relative time where precision does not matter.** With `numeric: "auto"` MDN's [RelativeTimeFormat examples](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) show "yesterday" instead of "1 day ago".
- **Never print 03/04/2026.** Write the month as a word ("4 Mar 2026") or let the user's locale pick the order. If you already support other languages, the MakerHunt piece on [localizing your product for a new language market](https://makerhunt.io/studio/how-to-localize-your-product-for-a-new-language-market) covers locale formats more broadly.
- **APIs and exports use ISO 8601 / RFC 3339.** The [RFC 3339 timestamp spec](https://www.rfc-editor.org/rfc/rfc3339) gives the familiar shape, for example `1985-04-12T23:20:50.52Z` for a UTC instant. If you are designing endpoints, the guide on [shipping a public API your first customers will use](https://makerhunt.io/studio/how-to-ship-a-public-api-your-first-customers-will-use) has the wider picture.

## Where do daylight saving time bugs come from?

Daylight saving time bugs come from assuming every day has 24 hours and every local time happens once.

- **The missing hour.** In the United States, according to [NIST's daylight saving time page](https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst), daylight saving time begins at 2:00 am on the second Sunday of March, when local time skips to 3:00 am. A 2:30 am New York job that day points at a time that does not exist.
- **The repeated hour.** It ends at 2:00 am on the first Sunday of November, when local time goes back to 1:00 am, so 1:30 am happens twice. This year that is Sunday, 1 November 2026.
- **The week when everyone's gap is different.** The EU switches back on the last Sunday of October, according to the [European Commission's summertime page](https://transport.ec.europa.eu/transport-themes/summertime_en), which was 25 October this year. Between that and 1 November, Berlin is five hours ahead of New York instead of the usual six. Hard-coded differences between zones are wrong that week every year.
- **Adding 24 hours is not adding a day.** Take 9:00 am in New York on Saturday 13 March 2027 and add 24 hours: you get 10:00 am on Sunday, because Sunday is only 23 hours long. Adding one calendar day in the zone gives 9:00 am, which is almost always what the user meant.
- **Not every zone changes.** `Asia/Kolkata` is UTC+5:30 all year, and `Asia/Kathmandu` is UTC+5:45. Code that assumes whole-hour offsets breaks.

Good tools make you choose what happens in the gap and overlap. Temporal's `ZonedDateTime` has a `disambiguation` option (earlier, later, compatible or reject); Python's datetime uses the `fold` attribute for the repeated hour. Pick a rule on purpose.

## How do you run scheduled jobs in your users' local time?

Your scheduler runs on one clock, often UTC. Your users live on many. Bridge them with data, not cron expressions.

1. **Store the user's intent:** "daily digest, 09:00, Europe/Berlin".
2. **Store the next run as a UTC instant** (`next_run_at`), computed from that intent.
3. **Run one frequent job**, say every five minutes, that picks up rows where `next_run_at` has passed, does the work, then computes the next local 09:00 in that zone and stores its UTC instant.

Recomputing from local time each run keeps the digest at 9 am through daylight saving and rule changes. A single cron line at 08:00 UTC drifts an hour for many users twice a year.

The same idea fixes "today" and "yesterday". A daily report for a user in Kolkata should cover midnight to midnight in `Asia/Kolkata`, which is 18:30 to 18:30 UTC. Compute the day's start and end in the user's zone, convert both to UTC, and query between them. Trial end times follow the same rule: decide whose midnight counts and show the zone.

## Which JavaScript Date traps catch most makers?

MDN's [Date reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) spells out the worst one: when no offset is given, date-only strings are read as UTC and date-time strings are read as local time. MDN calls it a historical spec error kept for web compatibility.

Run this in Node with `TZ=America/New_York` and you get:

- `new Date("2026-03-04")` printed Tue Mar 03 2026 19:00 Eastern. The date moved back a day.
- `new Date("2026-03-04T00:00")` printed Wed Mar 04 2026 00:00 Eastern. Same calendar day, different rule.

Two more traps:

- **Months start at zero.** MDN's [getMonth() reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMonth) confirms it returns 0 for January, so `new Date(2026, 3, 4)` is 4 April.
- **Only two zones.** As the Temporal page notes, a `Date` reads its parts only in UTC or the device's zone. "The hour in Berlin" needs `Intl` or a library.

The safe habit: send instants between server and browser as full RFC 3339 strings with `Z` or an offset, and keep plain dates as plain `YYYY-MM-DD` strings that you never pass to `new Date()`.

![Desk globe in front of a wall world map with a row of national flags along its edge](https://images.pexels.com/photos/7634295/pexels-photo-7634295.jpeg?auto=compress&cs=tinysrgb&w=1200&h=800&fit=crop)

## Which libraries help, and is Temporal ready yet?

`Intl` covers display. You want a library for arithmetic in a zone: "next 9 am in Berlin", "add one day".

- **Temporal** is the built-in replacement for `Date`, with separate types for instants (`Instant`), zoned times (`ZonedDateTime`) and plain dates and times (`PlainDate`, `PlainDateTime`). The [TC39 Temporal proposal](https://github.com/tc39/proposal-temporal) is at Stage 4 and lists it as shipped in Firefox 139, Chrome 144 and Node 26, with Safari not yet listed as shipped. MDN marks it "Limited availability", not Baseline, as of October 2026. Fine on a Node 26 server; check your browser audience or add a polyfill before using it in the front end.
- **date-fns** added first-class time zone support in version 4, per the [date-fns v4 release post](https://blog.date-fns.org/v40-with-time-zone-support/), through the `@date-fns/tz` package. The older `date-fns-tz` add-on for v3 uses the `Intl` API, so no zone data ships in your bundle.
- **Luxon** is an immutable date library with native time zone and `Intl` support, per the [Luxon documentation](https://moment.github.io/luxon/).
- **Day.js** is a small library, and its [timezone plugin](https://day.js.org/docs/en/plugin/timezone) (which depends on the utc plugin) adds `dayjs.tz` for converting between zones.
- **Python** has [the zoneinfo module](https://docs.python.org/3/library/zoneinfo.html) in the standard library since 3.9. It uses the system's IANA data and falls back to the `tzdata` package if there is none, handy in slim containers.

Pick one approach per codebase; mixing libraries brings the bugs back.

## How do you test time zone code before users find the bugs?

Your laptop's zone hides most of these bugs.

- **Run the suite in a non-UTC zone.** Node reads the `TZ` environment variable, per the [Node.js docs on the TZ variable](https://nodejs.org/api/cli.html#tz), so `TZ=America/New_York npm test` works. Run CI once in a zone west of UTC and once in a half-hour zone like `Asia/Kolkata`.
- **Freeze the clock.** Any code that calls "now" should take the current time as an argument or from one helper you can fake.
- **Keep a list of boundary dates** and test each: US and EU clock-change Sundays, New Year's Eve, 29 February, and month ends.
- **Test round trips.** Save, read back through the API, show, export. It should match what the user typed.

## A time zone audit sheet for your product

Before changing code, list every place a date lives.

Where

Field

Kind

Stored as

Shown in whose zone

Risk

Database

created_at

Instant

timestamptz

Viewer's

Low

Database

due_date

Plain date

date

None, show as is

High if stored as timestamp

Scheduler

Daily digest

Local time + zone

09:00 + IANA name, next_run_at

User's

High around DST

Calendar

Booked session

Future local time

Local time + IANA name

Host's and guest's

High if pre-converted

Reports

"Yesterday"

Day range

Computed per user zone

User's

Medium

Exports and API

All timestamps

Instant

RFC 3339 with Z or offset

None

Low

Anything marked High goes on day two of the plan below.

## A made-up example: Tidelatch fixes its reminders in a week

This story is invented to show the steps; Tidelatch and its founder are not real.

Solveig runs Tidelatch, a booking and reminder app for independent music teachers who teach students online. She lives in Berlin. One autumn, in the week between the EU and US clock changes, three teachers write in: New York students got reminders an hour early.

The cause: lessons were stored as UTC instants worked out at booking time from a fixed offset, and the reminder job added "six hours" for New York. The fix: lessons become local time plus the teacher's IANA zone, with the instant computed when the reminder is due. Each student gets their own zone, detected from the browser and confirmed on first booking. Every reminder email shows the time in both the teacher's and the student's zone. And her test suite now runs in `America/New_York` and `Asia/Kolkata`, with the next four clock-change Sundays in a fixture file.

The next spring, the week the clocks change, nothing about times reaches her inbox.

## What mistakes cause the most time zone bugs?

Weak

Better

Store the user's offset, like +05:30

Store the IANA name, like Asia/Kolkata

Store birthdays as midnight UTC

Store them in a date column

Convert future bookings to UTC at booking time

Keep local time plus zone, compute the instant later

One cron line at 08:00 UTC for "9 am digests"

Per-user next_run_at recomputed in their zone

Add 24 hours for "tomorrow"

Add one calendar day in the user's zone

Show 03/04/2026

Show 4 Mar 2026, or the locale's format

Tests only in UTC

Tests in a western zone, a half-hour zone and on DST dates

## A 7-day plan to fix date handling in your product

This is a planning guess for one maker working part of each day, not a benchmark.

Day

Work

Done when

1

Fill in the audit sheet

Every date field has a kind and a risk

2

Fix storage: timestamptz for instants, date for plain dates, an IANA zone column per user

Migration written and tested on a copy

3

Capture the zone, add the settings dropdown and first-schedule check

New and existing users have a zone

4

Move scheduled jobs to next_run_at in the user's zone

Digests and reminders follow local time

5

Display: Intl with the user's zone, zone names on time-sensitive screens

No ambiguous date left in the UI

6

Tests: TZ variants in CI, frozen clock, DST boundary fixtures

Suite passes in three zones

7

Exports and API in RFC 3339, watch support

A week of no time complaints

## Time zone FAQ

### Should I store all dates in UTC?

Store instants in UTC. Store plain dates as dates, and store future local events as local time plus an IANA zone. Converting everything to UTC causes the off-by-one-day bugs.

### Is it enough to save the user's UTC offset?

No. An offset is only correct for part of the year in zones with daylight saving time. Save the IANA name and derive the offset for any date.

### Can I use Temporal in production now?

On the server, if you run Node 26 or later, yes. In browsers, MDN still marks it as limited availability in October 2026, so check your users' browsers or use a polyfill.

## Get the clock right once

Date handling best practices come down to knowing what each value means and converting only at the edges. And when the product is ready for more makers to try, [MakerHunt](https://makerhunt.io/) is a friendly place to launch it.

## Links

- Article (HTML): https://makerhunt.io/studio/how-to-handle-time-zones-and-dates-in-your-product
- AI-friendly Markdown: https://makerhunt.io/studio/how-to-handle-time-zones-and-dates-in-your-product.md

## Explore

- [Studio index](https://makerhunt.io/studio)
- [Blog](https://makerhunt.io/blog)
- [Browse projects](https://makerhunt.io/projects)

_HTML version: https://makerhunt.io/studio/how-to-handle-time-zones-and-dates-in-your-product_