PostgreSQL: Timezone for DateTime is +0000 set by datetime_field

I have a datetime_field in my form. When I inspect the param send by this field, there is no timezone in it like "start_datetime"=>"2024-01-23T17:44". After saving the record to the database, the saved value for my field start_datetime is Tue, 23 Jan 2024 17:44:00.000000000 UTC +00:00. My current timezone should be +0100. If I’m doing DateTime.now.to_s it gives me 2024-01-23T17:57:49+01:00. When I inspect the created_at field on the new record, the datetime is also without a timezone: Tue, 23 Jan 2024 16:46:13.225547000 UTC +00:00. The created_at is one hour in the past in relevance to my start_datetime field (17:44 vs 16:46). I’m using PostgreSQL. When I view the data in pgAdmin 4, all datetime columns have the appendix timestamp without time zone (6). Has this something to do with PostgreSQL?

This Post don’t belongs to PostgreSQL, I have the same issue with SQLite. My Timezone is +0100. But after creating the record, the values are stored with the timezone +0000, when I query the records with rails.

When I’m using a DateTime-Picker in my form, set it to DateTime.now and instantly submit it, the values created_at and updated_at both are one hour behind the submitted value.

Is this normal behaviour, or do I have to setup something?

Regards, Manuel

Hi @manuelbeck , it is normal behavior. ActiveRecord stores timestamps in UTC time +000. It is advised that we leave that alone.

You can configure your app to use a specific time zone, which is has its drawbacks but is “ok” if your users are all in the same time zone.

See the other Date and Time methods that get you what you want/expect. For example, using Time.current instead of Time.now.

It's About Time (Zones).

1 Like

Hi @ybakos,

thank your for your reply!

I was already thinking about this, that the timestamp should better be in UTC.

I’m wondering why DateTime.now gives me a timestamp with my current timezone e.g. Fri, 26 Jan 2024 12:35:25 +0100 when i didn’t changed something in my config.

And second, when I store a DateTime in my Database the time is not adjusted for UTC. The time stays the same and only the Timezone is set to UTC.

Example: When I want to store 2024-01-26 12:41:00 it stores this as Fri, 26 Jan 2024 12:41:00.000000000 UTC +00:00, although my timezone is +0100 and created_at is stored as Fri, 26 Jan 2024 11:41:35.554085000 UTC +00:00, which would be correct.

Great questions. Full disclosure: I find time zones mind-boggling and confusing! But I’ll try to share what I have discovered.

I’m wondering why DateTime.now gives me a timestamp with my current timezone e.g. Fri, 26 Jan 2024 12:35:25 +0100 when i didn’t changed something in my config.

I believe the default behavior of Rails is to lean on the operating system time zone by default, unless a specific time zone is stated in the config. This is why DateTime.now is +0100. In my experience, expecting DateTime.now to always be in “my” time zone can become problematic because the server we deploy our application to may have a different time zone in its OS than what my local dev env is. For example, the Heroku env I deploy to is configured to be UTC +0.

And second, when I store a DateTime in my Database the time is not adjusted for UTC. The time stays the same and only the Timezone is set to UTC.

Hmm, I recommend double-checking this. If I use ActiveRecord to load a user, and inspect created_at, such as User.first.created_at we do see a time in our current time zone. But this is not the actual value stored in the database. If you check the table itself, you will see that the actual value stored in the table is in UTC+0. ActiveRecord “converts” timestamps stored in the database into your Rails application’s time zone.

Perhaps show the exact code that you are writing that stores a specific datetime, and loads that specific datetime. eg:

original_date_time = DateTime.current
Foo.create(my_date: original_date_time)
f = Foo.last
puts f.my_date # should be the same as `original_date_time`

And let’s see what we see.

1 Like

The initialize_timezone initializer is run by default at startup, unless you configure Rails otherwise.

This initializer:

Sets ActiveRecord::Base.time_zone_aware_attributes to true , as well as setting ActiveRecord::Base.default_timezone to UTC. When attributes are read from the database, they will be converted into the time zone specified by Time.zone .

The default_timezone config:

Determines whether to use Time.local (if set to :local) or Time.utc (if set to :utc) when pulling dates and times from the database. The default is :utc.

Hi @jamiemccarthy and @ybakos,

thank your for your explanations.

I found the article It's About Time (Zones) very helpful and explains everything.

After making some experiments with Times in Rails and reading some things, I have found out the following:

Time.now is always the system time where the rails application is running on. config.time_zone have no impact on this.

Time.current and Date.current uses the setting of config.time_zone which sets the timezone in Time.zone. The timezone in Time.zone can also be set manually. The default of config.time_zone is UTC.

When working with times in rails, Time.current and Date.current should be used, instead of Time.now and Date.today. Date.today also uses the system time.

ActiveRecord saves all timestamps in the database in UTC, no matter which timezone is set. You could change this with config.active_record.default_timezone if you want.

When you set config.time_zone, then all times, even created_at and updated_at, are in the configured timezone you want, but stored as UTC in the Database. DST is also correctly handled by Rails when converting the time to UTC.

If you want to let a user display the times in theire timezone, you can do this with an around_action in application_controller.rb

# app/controllers/application_controller.rb
around_action :set_time_zone, if: :current_user

private

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

I was wondering which values can be set in config.time_zone and is really hard to find the right vaues. In the article It's About Time (Zones) I found the command rake time:zones:all (which can also be written as rails time:zones:all) which output something like this:

* UTC -12:00 *
International Date Line West

* UTC -11:00 *
American Samoa
Midway Island

* UTC -10:00 *
Hawaii

I thought first, i have to write something like UTC -11:00 in config.time_zone, but that’s wrong. You can use the values after * UTC -11:00 * like American Samoa or Midway Island.

Another thing is, that the Ruby class DateTime is deprecated since Version 3.0: DateTime | Ruby API (v3.0). I thought, Rails will use the data type DateTime for the database values, because i’m using the datetime column type, but that’s not true. When I’m getting the class of a datetime column, the datatype is ActiveSupport::TimeWithZone and according to It's About Time (Zones) ActiveSupport::TimeWithZone and Time have the same API and are interchangeable. So it’s best to use Time.

There is much to know about it and there should be added a documentation in the ruby on rails guides for it.

Awesome summary, @manuelbeck . Thank you for taking the time to write about your findings. I learned some new things from you! I had no idea that DateTime was deprecated.

I think the reason we get surprised by Time.now and Date.today is that we learn this API early in our Ruby journey (many “beginner” examples show this API) and it ends up being a bad habit that surprises us later when we have to deal with time zones!

I would totally support a PR to the rails guides (not sure where) that consists of what you wrote in your post.

I was just looking at some code in an app I’m working on where I am using Time.zone.today to get a date. Would Date.current be exactly the same thing? Or are there subtle edge cases I am not thinking of here?

Awesome summary, @manuelbeck . Thank you for taking the time to write about your findings. I learned some new things from you! I had no idea that DateTime was deprecated.

Thank you, it’s good, when we can help each other :slightly_smiling_face:

I think the reason we get surprised by Time.now and Date.today is that we learn this API early in our Ruby journey (many “beginner” examples show this API) and it ends up being a bad habit that surprises us later when we have to deal with time zones!

I think this is it and even the rails guides uses Time.now at some points. But when you read the api documentation, you see that time calculation methos like Date.tomorrow, Date.yesterday are relying on the timezone set by config.time_zone or Time.zone when you look in the source for Date.tomorrow or Date.yesterday. So if you work with timezones or not, you have to know about it.

I would totally support a PR to the rails guides (not sure where) that consists of what you wrote in your post.

Very good. I will see, where i can do this :slight_smile:

Hi @walterdavis,

I was just looking at some code in an app I’m working on where I am using Time.zone.today to get a date. Would Date.current be exactly the same thing? Or are there subtle edge cases I am not thinking of here?

The API-Documentation says for Date.current

Returns Time.zone.today when Time.zone or config.time_zone are set, otherwise just returns Date.today.

So Date.current will return Time.zone.today when a timezone is set, otherwise the system date, but in rails a timezone is always set.

Thank you for asking this. I didn’t know, there is a Time.zone.today method.

I learned something new when doing Time.current, the class of the result is ActiveSupport::TimeWithZone, so rails converts always to TimeWithZone when working with timezones. But the class of Time.now is Time.

Adding my voice here, time zone stuff is hard when timestamps do not include TZ information. Honestly removing a few bytes from a timestamp is not worth the ambiguity!

As noted:

  • Ruby will use the system locale by default. When parsing timestamps on your local host that will be in your time zone. In a deployed environment it will almost certainly be UTC time. If you are using docker locally than I would expect that to be in UTC time as well.

  • Postgres, please use TZ aware types like timestamp(6) with time zone. Internally they are still transformed and stored in UTC time but there is no abiguity

“For timestamp with time zone, the internally stored value is always in UTC (Universal Coordinated Time, traditionally known as Greenwich Mean Time, GMT). An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone. If no time zone is stated in the input string, then it is assumed to be in the time zone indicated by the system’s TimeZone parameter, and is converted to UTC using the offset for the timezone zone.”

sourced from: PostgreSQL: Documentation: 16: 8.5. Date/Time Types

In this case I really dislike the behaviour of datetime-local in that it defaults to the browers time zone and as a developer we need to do extra work to figure out / record what that the timezone is!

If your application knows what the users timezone is (because it is configured) you can do something like:

Time.use_zone(TZInfo::Timezone.get(current_user.timezone) do

@start_at = Time.zone.parse(@start_at)

end

Otherwise you’ll need to follow the MDN recommendation and allow the TZ to be selected from another input field…

Hi @garden_gnome,

so you suggest, to use timestamp with time zone for PostgreSQL? The rails default is to always save timestamps as UTC in database and the SQL standard seems to be the same as noted in PostgreSQL: Documentation: 16: 8.5. Date/Time Types. I don’t the see any advantage why to save the timestamp with timezone in the databse. The databse get’s confusing, if you have timestamps with different timezones. Rails do the calculation for you when reading and writing to the databse, so why not use this feature? And if the timestamps are all in UTC in the database and you want to understand them, you have just to calculate from UTC in your timezone and not more. If the timestamps would be in different timezones, the calculation for you would be much harder.

You write, that

I really dislike the behaviour of datetime-local in that it defaults to the browers time zone

but according to the MDN-Docs, the datetime-local input is not aware of any timezone. You have to make the timezone calculation after. It says:

The control is intended to represent a local date and time , not necessarily the user’s local date and time . In other words, the input allows any valid combination of year, month, day, hour, and minute—even if such a combination is invalid in the user’s local time zone (such as the one hour within a daylight saving time spring-forward transition gap).

Instead of

Time.use_zone(TZInfo::Timezone.get(current_user.timezone)

you could just write

Time.use_zone(current_user.timezone)

you could just write

Time.use_zone(current_user.timezone)

Huh yeah you are correct! it accepts anything that Time.zone= accepts.

In regards to postgres both forms of a timestamp take up 8 bytes of space so size isn’t a consideration! and there are a heap of sources that recommend using timestamp with timezone like the postgres wiki

Or this issue in rails timestamp schema migration on postgres doesn't include timezone · Issue #21126 · rails/rails · GitHub which is worth a read.

Personally I expect to explicitly see TZ info even if it is always showing UTC one less thing to think about.

Cool thank you, I didn’t know there was such a discussion in rails about timestamp with zone. So the best would be that this is the default for rails with postgresql. Sad, that this isn’t already included in the defaults of rails 7.1. So, how I would write my migration for this, do I have to just use the datatype timestamptz instead of datetime or can I configure that the default of datetime for rails is timestamptz? I would like to leave my migrations database neutral.

I’m really sorry that I did not reply earlier. I totally missed your response, I expect that you already found how to do this but just in case anyone else lands here.

The rails API docs has some documentation on the datetime_type on the PostgreSQLAdaptor.

Specifically you can change the default using:

ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz

I’ve currently got that setup in /config/initializers/active_record.rb

Hi Jerome,

thank you for your response.

So it‘s possible by configuration, thank you very much. Currently I‘m using just Sqlite3, because Postgre is too big for me. I‘m just writing a little app for an association and maybe Sqlite3 is enough for it.

Greetings,

Manuel