Almost every serious date bug in production software comes from the same decision, made early and rarely revisited: what exactly gets written to the timestamp column. The advice to store dates in UTC is repeated so often that it has become a slogan, which is unfortunate, because the reasoning matters more than the rule and the rule has one genuine exception. A system that follows it blindly will still lose data on the two mornings a year when the clocks change.

This article explains what a bare local timestamp cannot express, why daylight saving produces both impossible and duplicated times, how database column types differ in what they actually keep, and what to record for an event that has not happened yet. Any instant can be inspected as a raw count of seconds with the Unix timestamp converter.

Why Should You Store Dates in UTC?

Because Coordinated Universal Time never shifts. It has no daylight saving, no political adjustments and no ambiguity, so an instant recorded against it means exactly one thing forever, regardless of where the reader or the server happens to be.

Everything else follows from that. 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. And 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.

What Goes Wrong With a Bare Local Time?

A local timestamp without an offset or a zone is incomplete data. It may describe an instant that never existed, or one that occurred twice, and there is no way to tell from the value which case you are in.

Take Europe/Berlin in 2026. On the morning of 29 March the clocks jump from 02:00 to 03:00, so 02:30 that day simply does not exist: any record claiming it is either wrong or was written by a system unaware of the transition. On 25 October the clocks fall back from 03:00 to 02:00, so 02:30 occurs twice, an hour apart, and a stored value of "2026-10-25 02:30" identifies two different instants without distinguishing them. These daylight saving gaps and overlaps are not edge cases in the theoretical sense; they hit every affected system twice a year, on schedule.

What Your Database Column Actually Stores

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 timestamptz: stores an instant normalised to UTC and converts on input and output using the session setting. It does not retain the zone you supplied, only the instant, despite the name.
  • PostgreSQL timestamp: stores the wall-clock reading exactly as written, with no conversion and no zone. Useful only when the local reading is the fact you care about.
  • MySQL TIMESTAMP: converts to UTC on write and back on read, but is limited to a range ending in January 2038, for reasons explained in the year 2038 problem.
  • MySQL DATETIME: stores the literal value with no conversion at all, which makes it a wall clock rather than an instant.
  • An integer column: a plain epoch count, unambiguous by construction, at the cost of being unreadable in a query result.

The recurring lesson is that a type storing an instant and a type storing a wall clock are different tools. 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.

Past Events and Future Events Are Different

This is where the slogan breaks down. For something that has already happened, UTC storage is complete and correct. For something scheduled in the future, an instant alone can become wrong, because the rules may change before the date arrives.

Consider a recurring 09:00 meeting in Lisbon booked for next November. Store it as an instant in UTC and it is fixed at 09:00 Western European Time; if Portugal alters its daylight saving arrangements before then, your meeting quietly moves to 08:00 or 10:00 local, which is not what anybody agreed. The correct record for a future local event is the local date and time together with the IANA zone identifier, such as Europe/Lisbon, with the instant computed at the moment it is needed. Governments change these rules with only weeks of notice, and the zone database is updated several times a year in response.

A Practical Rule

Five decisions cover almost every case, and they are easier to apply than to argue about.

  • Record past events as instants: in UTC, at whatever precision the domain needs.
  • Record future local events as local time plus a zone identifier: never as a bare instant, and never as an offset alone.
  • Keep the user's zone separately: as a named identifier, so you can render anything correctly later.
  • Store dates in UTC but display in local time: convert at the outermost layer, once, immediately before a person reads it.
  • Never store a bare local reading: a wall-clock string with no offset and no zone is incomplete data, and no amount of later processing can recover what it meant.
  • Use plain dates where there is no time: a birthday is a calendar date, not an instant, and converting it through a zone is how people end up a day younger.

That last point catches a surprising number of teams. A date with no time attached should be stored as a date, because passing it through UTC storage adds a fictional midnight that some conversions will then shift backwards across a day boundary. The instinct to store dates in UTC is right for instants and wrong for calendar values, and the distinction between the two is worth making explicit in your schema rather than leaving it to convention.

Store Dates in UTC, Display in Local Time

Once you store dates in UTC, everything user-facing becomes a formatting problem rather than a data problem. Send the instant to the client, let the client apply the reader's own zone and locale, and no server needs to know where anyone is. How browsers work out that zone is covered in how browsers determine local time, and the offset your own machine reports appears on the UTC offset detector.

Where a formatted string must cross a system boundary, write it with an explicit offset rather than assuming one. The conventions for that are set out in ISO 8601 explained. A timestamp that carries its own offset can be re-read by anything, in any region, with no shared configuration.

Conclusion

Store dates in UTC because Coordinated Universal Time is the only reference that never moves, and because a bare local reading cannot survive the two mornings a year when the clocks change. Record past events as instants, record future appointments as a local time plus a zone name, keep calendar dates as dates, and convert to local form only at the point of display. Check any stored value against the Unix timestamp converter, or see the current instant in every notation on localdatetime.now.