How to Convert a User's Local Time to UTC Before Saving It

Developer
scheduling

A user books a meeting for “3pm on Friday.” What gets written to your database depends on one question: did you convert their local time to UTC before saving it?

If not, you’ve stored 2026-06-20 15:00:00 with no time zone context. When you retrieve it and display it to another user — or the same user after a DST transition — the time will be wrong.

The rule: always store UTC. Always convert from the user’s IANA time zone at the point of input. Never store a fixed offset.

The correct input flow

User selects: Friday 20 June, 3:00pm
User's time zone: America/New_York (from browser or profile)

Convert at input:
  2026-06-20T15:00:00 America/New_York
  → 2026-06-20T19:00:00Z  (UTC, because EDT = UTC−4 in June)

Store: 2026-06-20T19:00:00Z

On retrieval, convert back to the user’s local time:

2026-06-20T19:00:00Z → 3:00pm America/New_York ✓
2026-06-20T19:00:00Z → 8:00pm Europe/London ✓  (for a different attendee)

Getting the user’s time zone

In the browser:

const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// → "America/New_York"

Send this to your backend alongside the datetime string. Don’t trust it blindly — let users confirm or override it in their profile. Some devices report incorrect zones.

Node.js with date-fns-tz:

import { fromZonedTime } from "date-fns-tz";

const localDateString = "2026-06-20T15:00:00"; // from user input
const timeZone = "America/New_York"; // from browser/profile

const utcDate = fromZonedTime(localDateString, timeZone);
// utcDate is a JS Date object representing 2026-06-20T19:00:00Z

Python (with zoneinfo, Python 3.9+):

from datetime import datetime
from zoneinfo import ZoneInfo

local_dt = datetime(2026, 6, 20, 15, 0, 0, tzinfo=ZoneInfo("America/New_York"))
utc_dt = local_dt.astimezone(ZoneInfo("UTC"))
# utc_dt → 2026-06-20 19:00:00+00:00

The DST ambiguity trap

On the night clocks fall back, 1:30am America/New_York occurs twice — once before the transition and once after. If a user picks a time in that repeated hour, you can’t know which occurrence they mean.

Most libraries default to the earlier (pre-transition) occurrence. Make this explicit in your code rather than relying on the default. For scheduling tools, the cleanest solution is to avoid offering slots in the ambiguous hour on DST-transition nights, or to flag them and ask the user to confirm.

What to store

  • A UTC timestamp (TIMESTAMP WITH TIME ZONE in PostgreSQL, UTC ISO 8601, or Unix epoch)
  • The user’s IANA zone string (America/New_York) alongside the event, for display
  • Not: a bare local datetime without zone, or a numeric offset like −04:00

The IANA zone is for display. The UTC timestamp is for comparisons, sorting, and calendar math.

Buy me a coffe