Home Latest

latest / Time.now lies in a Rails app

Every Rails app sits on a stack of time zones — the database, the server, your dev machine, the system clock, the user’s preference, the browser. They mostly agree. The bugs live at the boundaries where they don’t, and Ruby’s stdlib gives you just enough rope to mix them up.

The first rope is Time.now. It returns the system clock’s local time and ignores config.time_zone entirely. Time.current respects it.

Time.zone = "Europe/Stockholm"

Time.now      # => 2025-01-12 21:18:00 +0100 (server local)
Time.current  # => 2025-01-12 21:18:00 CET   (Rails zone)

On a UTC server this bites hard: Time.now hands you UTC while anything coming back through ActiveRecord arrives in the configured zone, and you end up comparing timestamps that look identical but aren’t.

The same trap shows up elsewhere:

  • Date.today — uses the system zone, so it can be a day off. Use Date.current.
  • Time.parse("2025-01-12 21:18") — silently assumes the system zone. Use Time.zone.parse(...).
  • Strings from external APIs — parse with an explicit offset: Time.strptime(str, "%Y-%m-%dT%H:%M:%S%z").in_time_zone.

The Rails-native helpers (1.day.ago, 2.hours.from_now, etc.) already use the configured zone, so they’re fine.

For per-user time zones, store the zone on the user and wrap the request:

around_action :use_time_zone

def use_time_zone(&block)
  Time.use_zone(current_user.time_zone, &block)
end

Two rules of thumb:

  1. In a Rails app, never type Time.now, Date.today, or Time.parse. Turn on the Rails/TimeZone rubocop cop so the linter catches them for you.
  2. Run your test suite in a zone that isn’t your dev machine’s. The Zonebie gem randomises it per run and surfaces the bugs you’d otherwise only see in production.

Most of this I learned from Nicklas Ramhöj’s Working with time zones in Ruby on Rails at Varvet — still the clearest write-up on the topic, more than a decade on.