Read more

Working with or without time zones in Rails applications

Arne Hartherz
June 12, 2017Software engineer at makandra GmbH

Rails supports time zones, but there are several pitfalls. Most importantly because Time.now and Time.current are completely different things and code from gems might use one or the other.

Illustration UI/UX Design

UI/UX Design by makandra brand

We make sure that your target audience has the best possible experience with your digital product. You get:

  • Design tailored to your audience
  • Proven processes customized to your needs
  • An expert team of experienced designers
Read more Show archive.org snapshot

Especially configuring an application that cares only about one time zone is a bit tricky.

The following was tested on Rails 5.1 but should apply to Rails 4.2 as well.

Using only local time

Your life will be easier if your application does not need to support time zones. Disable them like this:

config.time_zone = 'Berlin' # Your local time zone
config.active_record.default_timezone = :local
config.active_record.time_zone_aware_attributes = false

Note that we disable ActiveRecord's time zone logic while still configuring a time zone.
This seems odd at first, but is required for Time.current to work properly.

If possible, put your server into local time zone, since Ruby's Time will use that. In the example above, it should be in the Berlin zone.

Using time zones

If your application should support time zones, the default ActiveRecord configuration will respect a config.time_zone. If you do not configure a default time zone, all times will be UTC.

You need to use Time#in_time_zone or ActiveSupport::TimeWithZone#in_time_zone on all time objects to convert them to a user's current time zone.


Configuration Examples

Read on for examples on different configurations and what will happen and/or go wrong.

Without any time zone configuration

This happens:

  • ActiveRecord writes UTC to the database
  • ActiveRecord time attributes are ActiveSupport::TimeWithZone objects

But:

  • ActiveRecord will not convert time attributes. They will be UTC and you need to explicitly convert them.
  • Time.current will be UTC
  • Time.now will be your local time (or the local time of your server)
>> Project.create!
>> Project.connection.select_one('SELECT created_at FROM projects')
=> {"created_at"=>"2017-06-12 08:00:00.000000"}
>> Project.last.created_at
=> Mon, 12 Jun 2017 08:00:00 UTC +00:00
>> Time.now
=> 2017-06-12 10:00:00 +0200
>> Time.current
=> Mon, 12 Jun 2017 08:00:00 UTC +00:00

With a configured (default) time zone

config.time_zone = 'Berlin'

This happens:

  • ActiveRecord writes UTC to the database
  • ActiveRecord converts time attributes back to the configured default time zone
  • ActiveRecord time attributes are ActiveSupport::TimeWithZone objects
  • Time.current will be in the configured default time zone
  • Time.now will be your local time (or the local time of your server)

While this might be okay even for the "we don't need time zone" use case, UTC timestamps in the database are harder to read for humans.

>> Project.create!
>> Project.connection.select_one('SELECT created_at FROM projects')
=> {"created_at"=>"2017-06-12 08:00:00.000000"}
>> Project.last.created_at
=> Mon, 12 Jun 2017 10:00:00 CEST +02:00
>> Time.now
=> 2017-06-12 10:00:00 +0200
>> Time.current
=> Mon, 12 Jun 2017 10:00:00 CEST +02:00

When ActiveRecord's time zone logic is disabled, and no time zone is configured

config.active_record.default_timezone = :local
config.active_record.time_zone_aware_attributes = false

This happens:

  • ActiveRecord writes local time to the database
  • ActiveRecord time attributes will be read correctly in local time
  • ActiveRecord time attributes are Time objects

But:

  • Time.current will be UTC

While ActiveRecord seems to be working alright, using Time.current is incorrect in this case and would cause problems.

>> Project.create!
>> Project.connection.select_one('SELECT created_at FROM projects')
=> {"created_at"=>"2017-06-12 10:00:00.000000"}
>> Project.last.created_at
=> 2017-06-12 10:00:00 +0200
>> Time.now
=> 2017-06-12 10:00:00 +0200
>> Time.current
=> Mon, 12 Jun 2017 08:00:00 UTC +00:00

When ActiveRecord's time zone logic is disabled, and a local time zone is configured

config.time_zone = 'Berlin'
config.active_record.default_timezone = :local
config.active_record.time_zone_aware_attributes = false

This happens:

  • ActiveRecord writes local time to the database
  • ActiveRecord time attributes will be read correctly in local time
  • ActiveRecord time attributes are Time objects
  • Time.current will be in the configured (local) time zone

As described above, this is our suggested configuration for an application that needs to support only one time zone.

Note that you can not distinguish times around a DST switch from summer time to winter time, as any time between 2 and 3 AM could be either!

>> Project.create!
>> Project.connection.select_one('SELECT created_at FROM projects')
=> {"created_at"=>"2017-06-12 10:00:00.000000"}
>> Project.last.created_at
=> 2017-06-12 10:00:00 +0200
>> Time.now
=> 2017-06-12 10:00:00 +0200
>> Time.current
=> Mon, 12 Jun 2017 10:00:00 CEST +02:00
Arne Hartherz
June 12, 2017Software engineer at makandra GmbH
Posted by Arne Hartherz to makandra dev (2017-06-12 12:00)