Should You Store Dates in UTC? Yes — Here's Why and How
Always store timestamps in UTC. Here is what that means in practice for PostgreSQL, MySQL, SQLite, and JavaScript — and what to do with user-local times.
Always store timestamps in UTC. Convert to the user's local time zone only when displaying. This rule holds for every database, every backend language, and every time zone. The one exception: when you need to store a future local time (like a recurring alarm) where the user's intention — not the absolute moment — is what matters.
Convert a Unix timestamp →Ask ten developers how they store dates and you'll get ten different answers. Some store Unix timestamps (integers). Others use ISO 8601 strings. Some rely on their database's TIMESTAMP WITH TIME ZONE type. Others use TIMESTAMP (without timezone) and just remember that it's UTC. Then there are the databases with records in five different time zones because someone ran migrations from a laptop in a hotel in Tokyo.
All of these approaches can work. Most of them work badly in at least one edge case. The simple, universal rule — store everything in UTC, convert on display — exists because it eliminates an entire class of bugs that are otherwise very hard to find and reproduce.
Why UTC?
- UTC never changes. It has no daylight saving time, no political clock shifts, no historical amendments.
- All comparisons are simple. "Is event A before event B?" is a single integer comparison if both are stored in UTC.
- UTC is what Unix timestamps are. Every language, every database, every API returns time relative to UTC epoch.
- Replication and backups work correctly. A row written by a server in UTC+9 and read by a server in UTC-5 produces the same result.
- Auditing is unambiguous. A log entry at "2026-03-08T02:30:00Z" means exactly one moment in history, regardless of where it is read.
PostgreSQL
PostgreSQL has two timestamp types: TIMESTAMP (no time zone information stored) and TIMESTAMPTZ (timestamp with time zone). Despite the name, TIMESTAMPTZ does not actually store a time zone — it stores UTC. When you insert a timestamp with a zone offset, Postgres converts it to UTC and discards the offset. When you read it, Postgres converts it to the session's time zone for display.
Best practice: always use TIMESTAMPTZ. Set your session and server time zone to UTC (SET TIME ZONE 'UTC' or timezone = 'UTC' in postgresql.conf). Then every timestamp you insert is stored as UTC, and every timestamp you read is in UTC. Timezone conversion happens in the application layer, not in the database.
| Type | Stores zone? | DST safe? | Recommendation |
|---|---|---|---|
| TIMESTAMP | No | Depends on convention | Only if you enforce UTC by convention |
| TIMESTAMPTZ | No (converts to UTC) | Yes | Preferred — always stores UTC |
| DATE | No | N/A — no time component | Use for calendar dates without time |
MySQL and MariaDB
MySQL's TIMESTAMP type converts to UTC on storage and back to the current session time zone on retrieval — similar to PostgreSQL's TIMESTAMPTZ, but with a narrower range (1970–2038). The DATETIME type stores exactly what you give it with no conversion, making it the source of most MySQL timezone bugs: if you insert a value in EST without converting first, it is stored as EST and retrieved as EST — even if your server later moves to UTC.
Best practice with MySQL: set the server and session time zone to UTC, use TIMESTAMP for any moment-in-time (events, created_at, updated_at), and use DATETIME only for calendar-like data where the absolute UTC moment is not meaningful — such as a user's recurring alarm that should fire at 8 AM their local time regardless of DST.
SQLite
SQLite has no native date/time type. Dates are stored as TEXT (ISO 8601 strings), REAL (Julian day numbers), or INTEGER (Unix timestamps). Of these, INTEGER Unix timestamps are the simplest and most portable. They are always UTC, trivially comparable, and understood by every language's standard library.
If you use TEXT format, always store ISO 8601 with a Z suffix (2026-06-15T14:30:00Z) to make the UTC intent explicit. SQLite's strftime() and datetime() functions treat TEXT dates without a timezone suffix as UTC by default, but explicit is safer.
The Exception: Future Local Times
There is one legitimate case for storing a time in a local zone rather than UTC: future recurring events tied to a user's intent, not a fixed moment. A user who sets an alarm for '8:00 AM every weekday' means 8 AM in their local time zone — regardless of whether DST has shifted. If you store this as a UTC timestamp, the alarm will drift by one hour twice a year when DST changes.
The correct approach: store the wall-clock time (08:00), the IANA time zone ('America/New_York'), and the recurrence rule separately. Compute the next UTC fire time at scheduling time, and recompute after each DST transition. This is what calendar applications like Google Calendar do internally.
Practical Checklist
- Set your database server's time zone to UTC (postgresql.conf, my.cnf, or environment variable).
- Set your application server's time zone to UTC (TZ=UTC environment variable).
- Use TIMESTAMPTZ in Postgres, TIMESTAMP in MySQL (with UTC session), INTEGER in SQLite.
- Always pass timestamps to your database as UTC strings (2026-06-15T14:30:00Z) or Unix epoch integers.
- Store the user's IANA time zone ('America/New_York') separately in their profile.
- Convert to local time only in the presentation layer, right before rendering.
- Never store offsets (+05:30) as a substitute for a time zone identifier — offsets change with DST.
Frequently asked questions
- What is the difference between UTC and a Unix timestamp?
- A Unix timestamp is an integer representing seconds (or milliseconds) since January 1, 1970 at 00:00:00 UTC. UTC is the time standard. A Unix timestamp is the most common way to represent a UTC moment as a number. They are closely related but not the same: UTC is a standard, Unix timestamp is a data format.
- Should I store created_at and updated_at in UTC?
- Yes, always. These columns record moments in time — when something happened — not local calendar dates. Store them as TIMESTAMPTZ (Postgres) or TIMESTAMP with UTC session (MySQL). In application code, always pass UTC and convert to local only when displaying to the user.
- What happens if I store a timestamp in the wrong timezone?
- You get data corruption that is hard to fix. A row stored as 2:30 AM EST when the system expected UTC will be off by 5 hours for all future queries. Fixing it requires knowing which rows were written under which assumption — which is often unknown after the fact. Always enforce UTC at the infrastructure level, not just by convention.
- Do I need to worry about UTC leap seconds?
- Almost certainly not. Unix timestamps and all major databases (PostgreSQL, MySQL, SQLite) ignore leap seconds — they treat every minute as exactly 60 seconds and smear any adjustment across a longer window. Unless you are writing GPS software or operating a financial exchange that requires sub-second precision across the leap second boundary, you can safely ignore this.
We build practical, free time and date tools at epochcalc.com — every calculation runs in your browser using IANA tzdb via Luxon, so DST and zone math are correct by construction.