Past events are instants. Future events are local times. Store the fact you need, and let the zone rules do their work when the event happens.
The advice "always store UTC" is repeated so often it has become a reflex, but a reflex is not a reason. The reason UTC works for past events is that UTC never shifts. It has no daylight saving, no political adjustments, no ambiguity. A moment recorded against UTC means exactly one thing forever, no matter where the reader or the server happens to be.
Two records stored in UTC can be compared, sorted, and subtracted without consulting any rule set. Logs from machines in four countries merge into one correct sequence. A server can be moved between regions without rewriting its history, because nothing about the data depended on where it was written. A value written today still resolves correctly in ten years, even if the government where it was recorded has changed the rules twice in the meantime.
But here is the catch. The rule has one genuine exception, and a system that follows it blindly will still lose data on the two mornings a year when the clocks change.
When UTC is the right call: past events
For anything that has already happened, UTC storage is complete and correct. The event occurred at a specific moment. That moment can be represented as a single number: the count of seconds since the Unix epoch, 1 January 1970, or an ISO 8601 string with a trailing Z. Either representation identifies the moment unambiguously.
Consider a server log entry written at 14:30 local time in New York on a July afternoon. That local time corresponds to 18:30 UTC. If the log stores the UTC value, then any other system on Earth can read it, convert it to its own local time, and know exactly when the event happened. No further information is required.
Compare that to storing "2026-07-15 14:30" with no zone. A reader in London will assume one moment. A reader in Tokyo will assume another. A reader who knows the writer was in New York can reconstruct the moment, but only if they also know whether New York was observing daylight saving time on that date. That is a lot of knowledge to require from a future reader who may not even know the writer was in New York.
The same logic applies to financial transactions, audit records, sensor readings, and any other data that describes a completed event. The moment is the fact. UTC captures the moment. Everything else is presentation.
When UTC is wrong: future events and changing rules
Now consider a future event. A meeting scheduled for 9:00 AM local time in a specific city. If you store that as a UTC moment, you are making a bet. You are betting that the city's time zone rules between now and the meeting date will remain exactly as they are today.
That bet fails all the time. Governments change daylight saving rules on short notice. They abandon DST entirely. They move themselves across the international date line. Each of those changes shifts the UTC offset for the local time, which means the UTC moment you stored for "9:00 AM local" becomes wrong.
The classic example is a meeting scheduled for 2:30 PM on a date when the government decides, with six weeks of notice, to end DST a month later than previously planned. Your stored UTC moment now points to a different local time than the one your users expect. The meeting is at the wrong hour.
What should you store instead? The local wall-clock time plus the IANA time zone name, for example "2030-11-03 14:30 America/New_York". That representation survives rule changes, because the rule set is evaluated at the moment the event occurs. If New York changes its DST rules, the stored value still resolves to the correct local time under the new rules. The moment shifts, but the local experience does not.
This is the exception that proves the rule. The rule is not "always store UTC." The rule is "store enough information to reconstruct the event correctly, and choose the representation based on whether the event is past or future."
What your database column actually stores when you store dates in UTC
Column types are less alike than their names suggest, and choosing the wrong one quietly discards information. It is worth knowing precisely what your database does before trusting it to store dates in UTC on your behalf.
PostgreSQL: TIMESTAMP WITH TIME ZONE vs WITHOUT
PostgreSQL's TIMESTAMP WITH TIME ZONE (often abbreviated timestamptz) stores a moment normalised to UTC. On input, it converts the value to UTC using the session's time zone setting. On output, it converts back to the session zone. The original zone is not retained; only the moment survives.
TIMESTAMP WITHOUT TIME ZONE (often timestamp) stores the wall-clock reading exactly as written. No conversion happens on input or output. The value "2026-10-25 02:30" is stored as "2026-10-25 02:30" regardless of the session zone. This type is useful only when the local reading is the fact you care about, and you accept that the value carries no zone information at all.
The trap appears when developers choose timestamp thinking it is a smaller or simpler version of timestamptz. It is not. It is a different data type with different semantics. Mixing them in one schema, or migrating between them without a conversion, is a reliable way to shift a subset of your data by an hour.
MySQL: TIMESTAMP vs DATETIME
MySQL's TIMESTAMP type converts to UTC on write and back to the session time zone on read. Internally it stores UTC. The catch is its range: it ends in January 2038, a limitation inherited from the 32-bit Unix time representation. Any system still using this type for new data is building a time bomb.
MySQL's DATETIME stores the literal value with no conversion at all. It is a wall clock, not a moment. "2026-10-25 02:30" means whatever the application says it means, and nothing in the database records what that is.
The naming is unfortunate. In PostgreSQL, TIMESTAMP is the wall clock and TIMESTAMP WITH TIME ZONE is the moment. In MySQL, DATETIME is the wall clock and TIMESTAMP is the moment (with a range problem). If you memorise the PostgreSQL semantics and apply them to MySQL, you will be wrong.
SQLite: No native time zone support
SQLite has no dedicated date-time type. Dates are stored as text, as integers (Unix time), or as real numbers (Julian day). None of these carry zone information. The text representation "2026-10-25 02:30:00" is just a string. Whether it represents UTC, local time, or something else is entirely up to the application.
SQLite does provide date functions that assume UTC unless told otherwise. The datetime() function with no modifier interprets its input as UTC. This is a reasonable default, but it means a developer who stores local time and forgets the conversion will be off by the zone offset, silently, for every query.
When does storing dates in UTC fail at DST transitions?
Here is the problem that no amount of UTC storage solves, because the problem is in the local time itself. During the fall-back DST transition, the clock moves from 03:00 to 02:00. That means the hour between 02:00 and 03:00 occurs twice. The wall-clock reading "2026-10-25 02:30" identifies two different moments, one hour apart. A bare local timestamp cannot distinguish them.
If you store that local value without a zone and without a DST flag, you have stored an ambiguity. A reader cannot tell which of the two moments is meant. If you store it with a zone name but no offset, you have the same problem, because the zone alone does not tell you which occurrence of the ambiguous hour was intended.
The correct representation for a local time in an ambiguous period is the local time, the zone name, and the offset that was in effect. For example: "2026-10-25 02:30-04:00" versus "2026-10-25 02:30-05:00". Both are valid local times in the same zone, one hour apart. The offset disambiguates them.
The spring-forward transition creates the opposite problem. The clock moves from 02:00 to 03:00, so the hour between 02:00 and 03:00 never occurs. "2026-03-08 02:30" is an invalid local time. It did not happen. A user who enters that time probably made a mistake, but the system must decide what to do: reject the input, shift it forward, or guess.
The "always store UTC" rule and its exceptions
The rule "always store UTC" is correct for past events. It is wrong for future events. The distinction is not pedantic. It is the difference between recording history and scheduling the future.
For past events, UTC is lossless. The moment is fully specified, comparable, sortable, and portable. No other representation gives you all of those properties for free.
For future events, UTC is lossy in a specific way. It bakes in the current rule set, and rule sets change. The correct representation for a future event is the local time, the IANA zone name, and optionally the expected offset at the time of the event. The expected offset is a hint; the zone name is the source of truth.
There is a middle ground for events that are scheduled in local time but must be compared across zones. Store both: the local time and zone for the scheduling decision, and a computed UTC moment for the comparison. If the rules change before the event, the UTC moment is recomputed from the local time. The stored UTC value is a cache, not the source of truth.
How to choose a storage strategy for dates in UTC
The decision tree is short and practical.
For past events, store UTC. Use a column type that stores a moment, such as PostgreSQL timestamptz or MySQL TIMESTAMP (with awareness of the 2038 range). Never store a past event as a bare local time; the moment is the fact, and local time is presentation.
For future events, store the local wall-clock time and the IANA zone name. Use two columns, or a single column with a structured type if your database supports it. The zone name is not optional; an offset alone is insufficient because the offset can change between now and the event.
For recurring events, store the local time and the zone, and evaluate the moment at the moment of each occurrence. A meeting at 9:00 AM every Monday in New York shifts its UTC moment when DST changes. The local time is the invariant; the moment is derived.
For ambiguous local times, store the offset as well as the zone. This applies to both past and future events, but it is more common in scheduling because the ambiguity is visible at the time the event is created.
When you must compare a future event to a moment, compute the UTC value from the local time and zone at the time of comparison. Do not store that computed value as the source of truth unless you accept that it will be wrong if the rules change.
Which columns and formats to use when you store dates in UTC
| Situation | What to store | Example |
|---|---|---|
| Past event (log entry, transaction, reading) | UTC moment | 2026-10-25T06:30:00Z |
| Future event, fixed local time | Local time + IANA zone | 2026-10-25 02:30 America/New_York |
| Future event, fixed moment | UTC moment | 2026-10-25T06:30:00Z |
| Ambiguous local time (fall-back) | Local time + zone + offset | 2026-10-25 02:30-04:00 |
| Invalid local time (spring-forward) | Reject or shift forward | , |
| Recurring event | Local time + zone | 09:00 America/New_York, weekdays |
The shortest version of the rule: past events are moments; future events are local times. Store the fact you need, and let the zone rules do their work when the event happens.