Time Zones in Rails

Ale Solano
Updated April 17, 2024

We received this message some months ago:

Hi! I just added the Intended Use document in Formwork. I noticed that the time stamp is wrong (15:07 instead of 17:07). Can this be fixed? Important, because work was done out of business hours.

Hm. Weird. That difference of exactly two hours is screaming “time zone” to me. But how come is this happening? Our client is based in Germany, just like us. And our code explicitly sets the application time zone to “Europe/Berlin”. I don’t get it. Maybe he signed the document from another time zone? Or maybe our code is just not working as expected. This requires research.

I guess it’s time to learn about time zones in Rails.

Time zones in Rails

Formwork, our eQMS, is built with Rails. A Rails app works with three (+ one) different “times”. I mean, they all refer to the same time, but each one is expressed based on an independent—and possibly different—time zone.

  • System time. It uses the time zone set in your server. It’s the time you get when you call Time.now and it is represented by an Time object.
  • Application time. It uses the time zone set by the Rails application in config.time_zone. It’s the time you get when you call Time.zone.nowTime.current or any expression like 2.hours.from_now and it is represented by an ActiveSupport::TimeWithZone object.
  • Database time. It uses the time zone set in the database, which should always be UTC. It’s the time you get when you run a SQL query.
  • (Bonus) Browser time. It uses the time zone set in the browser. It’s the time rendered by local_time.

If you are working with time in raw Ruby, you’ll probably be using TimeDateTime and Date. Understandably, they all use system time. System time is OK but it’s a bit cumbersome if you suddenly want to work in a different time zone, as you’d need to change the TZ environment variable of your machine.

That’s why Rails introduced its own version of Time: ActiveSupport::TimeWithZone. It provides the same API than Time’s, making Time and TimeWithZone instances interchangeable. Well, kind of interchangeable. I found some aspects where they differ, and it’s a bit annoying. More about this later.

But the good thing about TimeWithZone is that you can easily change from one time zone to another by setting Time.zone to your favorite time zone identifier, like “America/New_York” or “Europe/Berlin”.

Rails uses TimeWithZone objects all across its layers. When you run User.first.created_at in your console, you get a TimeWithZone object, with the time offset corresponding to the time zone your set in your application. Indeed, this is what we call the application time. You can easily define the application time by tweaking config.time_zone in the config/application.rb file.

Though when Rails stores those times in the database, they are first transformed to UTC, the database time. Database time is UTC by default and should be left that way.

From system time to database time

Let’s see some examples in the console and add some color to this article.

Time.now uses the system time and returns a Time object.

> Time.now
 => 2023-07-10 06:19:05.0988883 +0000
 > Time.now.class
 => Time

If we change the timezone in our machine…

$ export TZ="America/New_York"

…we get the time in that timezone.

> Time.now
 => 2023-07-10 02:19:23.870709341 -0400

To get the application time we need to call Time.zone.now, getting a TimeWithZone object.

> Time.zone.now
 => Mon, 10 Jul 2023 09:19:06.363576224 CEST +02:00
 > Time.zone.now.class
 => ActiveSupport::TimeWithZone

It’s expressed in CEST+2 because we set the application time zone to “Europe/Berlin” in our config file.

> Time.zone
 => #<ActiveSupport::TimeZone:0x00007f95b03110f8
  @tzinfo=#<TZInfo::DataTimezone: Europe/Berlin>,

All these expressions result in TimeWithZone objects. And in fact, the recommendation is to always use application time expressions. Never use Time.now, always use Time.zone.now.

> Time.zone.now.class
 => ActiveSupport::TimeWithZone
 > Time.current.class
 => ActiveSupport::TimeWithZone
 > 2.hours.from_now.class
 => ActiveSupport::TimeWithZone
 > User.firt.created_at.class
 => ActiveSupport::TimeWithZone

Now, to get the database time we need to run SQL queries. Let’s retrieve a random User from the database.

> ale = ActiveRecord::Base.connection.execute("
   SELECT * FROM users
   WHERE email = '[email protected]'

Using SQL queries, all times are database times.

> database_time = ale["created_at"].to_s
 => "2023-01-16 22:09:13 UTC"

Formats: where the pain begins

Let’s try to transform that database time into application time. For that, we can use Time.zone.parse.

application_time = Time.zone.parse(database_time)
 => Mon, 16 Jan 2023 23:09:13.000000000 CET +01:00

Interestingly, the displayed format has changed! Now we get a more narrative expression of the same time.

We can see that if we retrieve the same value with Rails via ActiveRecord queries, the result is the same.

> User.find_by(email: "[email protected]").created_at
 => Mon, 16 Jan 2023 23:09:13.000000000 CET +01:00

Now, to get the time according to our server we will parse the database time with Time.parse and then call the getlocal method.

> system_time = Time.parse(database_time).getlocal
 => 2023-01-16 17:09:13 -0500

Oh, what? Time.parse maintains the same format. But TimeZone.parse did not! Hm, I don’t like that too much. It’s kind of inconsistent.

Don’t panic yet, there’s still a handy way of reverting the format of our application time back to the database format: using TimeWithZone#to_formatted_s(:db).

> application_time.to_formatted_s(:db)
 => "2023-01-16 22:09:13"

Aweso…Hey, the time zone has changed to UTC as well! Why? I was not expecting that.

OK, I sort of can understand the rationale behind it. The intention of the method is to mimic the time format in the database, which one could argue includes using the UTC timezone. Though I would say it should not be that way.

The main problem is that again this is not consistent with how Time uses to_formatted_s(:db). For Time objects, the same method does not bring the time zone back to UTC. It stays loyal to the system’s time zone.

> system_time.to_formatted_s(:db)
 => "2023-01-16 17:09:13"

Going back to this message,

Hi! I just added the Intended Use document in Formwork. I noticed that the time stamp is wrong (15:07 instead of 17:07). Can this be fixed? Important, because work was done out of business hours.

we can now see that TimeWithZone#to_formatted_s(:db) is the one to blame here. We are displaying times in UTC without informing the user. This is not what we want: the user should see the time according to his time zone. Or otherwise be informed that the time is UTC.

More users, different time zones

Everything gets more complicated as users start coming from different time zones. I can see how we can have more messages like that in the future.

So our first decision here is to start using local_time to display times in Formwork. local_time reads the browser’s time zone and updates the HTML directly on the client side using JavaScript, setting all times tagged with data-local="time" to the user’s time zone.

What about exports? Should we show times in the user’s time zone? At the end, we don’t know the time zone of the person who will be reading the export. So our second decision is to show UTC times in exports while explicitly stating that they are UTC times.

This means times will be either on the user’s time zone (browser time) or they will be UTC (database time). Application time is never considered.

So… what’s the purpose of application time?

Good question.

My understanding is that the main purpose of the application time is to set a default display time zone. This is really handy if

  1. all your users are in the same time zone because your display is correct without using JS.
  2. all your developers are in the same time zone because the console speaks our same time language.

And, well, the first point is not true and the second is kind of a temporary thing as well.

Also, our preferred format in Formwork is :db. And as we saw, that format reverts TimeWithZone objects (application time) back to UTC time. The application time is actually not displayed anywhere!

Bye, bye, Miss Application Time

After all this research, my understanding is that the best thing is to forget about application time and use UTC by default. We don’t gain too much by setting config.time_zone to “Europe/Berlin” as we have clients outside that time zone. Only sometimes we will display times in the browser time zone, which will be handled by local_time. But never in the application time zone.

So bye bye, config.time_zone = "Europe/Berlin".

Bye bye, application time.

On a slighty different note: You want to get your medical software certified under MDR but don’t know where to start? No worries! That’s why we built the Wizard. It’s a self-guided video course which helps you create your documentation yourself. No prior knowledge required. You should check it out.

Or, if you’re looking for the most awesome (in our opinion) eQMS software to manage your documentation, look no further. We’ve built Formwork, and it even has a free version!

If you’re looking for human help, did you know that we also provide consulting? We’re a small company, so we can’t take on everyone – but maybe we have time for your project? We guide startups from start to finish in their medical device compliance.

Congratulations! You read this far.
Get notified when we post something new.
Sign up for our free newsletter.


Leave the first comment