Ruby & Rails

Time zones: store UTC, and know which "now" you called

Most time-zone bugs come from two nearly identical methods and one place where dates are not really times.

10 June 2026 2 min read Mohammad Aaquib Jawed

Time bugs are seasonal. Everything works for months, the clocks change, and a report double-counts an hour.

The two methods

Ruby's Time.now returns the server's local time. Rails' Time.current returns the time in the application's configured zone.

In production these are often both UTC and behave identically, which is precisely why the bug survives development. On a developer machine in London during summer, they are an hour apart.

The same applies to Date.today versus Date.current. Use the Rails versions, always. A linter rule for this is worth more than remembering.

Storing and rendering

Store UTC. Timestamps go into the database in UTC and stay that way. This is the default and it should not be fought.

Convert only at the edges — when rendering for a human, or parsing input from one. A value should spend its entire life in UTC and be converted at the moment of display.

For a user-facing application, the zone that matters is usually the *user's*, not the server's, which means storing a zone preference and wrapping rendering in it rather than relying on a global setting.

Convert at the edges, never in the middle. A timestamp that has been converted twice is unrecoverable.

Dates are not timestamps

"Everything from 1 March" is not a timestamp comparison. Depending on zone, the boundary moves by hours, and rows near midnight fall on the wrong side.

Be explicit about which zone defines the day, convert the boundary to UTC, and compare against that. Use a half-open range — greater than or equal to the start, strictly less than the end — because inclusive ranges on timestamps either miss the final moment or double-count it.

That half-open habit alone removes a whole family of off-by-one reporting bugs.

The awkward realities

Days are not always 24 hours. When clocks go forward, one day is 23 hours long, and any code that adds 86,400 seconds to get "tomorrow" is wrong twice a year. Use date arithmetic, which understands this, rather than second arithmetic, which does not.

Some times do not exist. When the clock jumps forward, the skipped hour never happens — parsing a timestamp in it is an error, not an edge case to ignore. Others happen twice, which is why an ambiguous local time cannot be converted to UTC without extra information.

None of this is exotic. It is simply what time is, and the cost of ignoring it is a bug that only appears in March and October.

All writing Reply by email