Rails time zones: Ruby on Rails best practices explained
Time handling is one of those things that seems straightforward until your application spans multiple regions and users start reporting that events appear in the wrong time slot. We've all been there—a booking scheduled for 2 PM appears at 3 PM for half your user base, or batch jobs run at unexpected hours because the server drifted into daylight saving time. Rails time zones: Ruby on Rails best practices explained breaks down the patterns that keep your temporal data consistent, predictable, and correct across every deployment and every user's local clock.
Time zone handling in Rails requires coordinating three separate concerns: what gets stored in the database, what the application uses for calculations, and what gets displayed to end users. Get these three things aligned, and time handling becomes boring—which is exactly what we want.
config.time_zone for display
Your Rails application has a default time zone configuration that affects how Time objects are displayed and interpreted in views and logs. This is set in config/application.rb and typically defaults to UTC.
# config/application.rb
config.time_zone = 'Eastern Time (US & Canada)'This setting controls how Time.current behaves, how times render in views, and how timestamp columns appear when you inspect them in logs. It does not affect what gets written to the database—that's always UTC—but it changes how the application interprets time values after they're loaded from the database.
Think of it as the "default lens" through which your application views time. If you're building a service for a single region, setting this to your primary user's time zone makes sense. For a ride-hailing platform with operations across North America, you might set it to UTC and let the front-end handle localization, or you might set it to your company's headquarters time zone as a sensible default.
Here's what this means in practice. When you log a timestamp, you'll see it in the configured time zone:
# config/application.rb has config.time_zone = 'Eastern Time (US & Canada)'
# In your model or controller
event = Event.create!(scheduled_at: Time.current)
puts event.scheduled_at
# => 2024-01-15 14:30:00 -0500
# In logs, Rails will show times in Eastern Time
# [2024-01-15 14:30:00 -0500] Rails starts...This makes debugging easier because developers see times in a consistent zone. Just remember: changing config.time_zone doesn't change what's in the database (still UTC), and it doesn't change how times are transmitted over APIs (still ISO 8601 UTC).
Store UTC in the database
The first rule of time handling: always store everything in UTC. This is non-negotiable. Your database columns should be in UTC, your APIs should transmit UTC, and your logs should record UTC offsets clearly. UTC is the lingua franca that lets different systems, regions, and time zones talk to each other without confusion.
When Rails creates or updates a timestamp column, it automatically converts from the application's config.time_zone to UTC before inserting:
# config/application.rb: config.time_zone = 'Eastern Time (US & Canada)'
# You create a timestamp in the Rails app (in Eastern Time context)
booking = Booking.create!(reserved_at: Time.current)
# Time.current is 2024-01-15 14:30:00 -0500
# What gets stored in the database
# reserved_at column: 2024-01-15 19:30:00 (UTC)
# When you retrieve it
puts booking.reserved_at
# => 2024-01-15 14:30:00 -0500 (Rails converts back to config.time_zone)This conversion is automatic and transparent. The key is to trust that Rails is doing it correctly—and to verify it occasionally by checking your database directly:
# In your database (e.g., PostgreSQL)
SELECT reserved_at FROM bookings LIMIT 1;
-- 2024-01-15 19:30:00+00
# In Rails console (when config.time_zone = 'Eastern Time')
Booking.last.reserved_at
# => 2024-01-15 14:30:00 -0500Why does this matter? Because when you query for times, compare timestamps, or write batch jobs that run on a schedule, you're working with UTC values. A job that runs "at midnight" needs to be clear: midnight in which zone? The answer is always UTC in your database, then you apply user-specific or region-specific logic on top.
For example, if you're building a daily digest email system for users across multiple regions, you'd query the database for events created between two UTC timestamps, then filter or sort by each user's local time zone in the application layer:
# Find events created in the last 24 hours (UTC)
events = Event.where(created_at: 24.hours.ago..Time.current)
# For each user, apply their time zone
user.events_digest(events) do |evt|
# This render respects user.time_zone
evt.created_at.in_time_zone(user.time_zone).strftime('%I:%M %p')
endTime.current instead of Time.now
One of the most common mistakes we see in Rails codebases is using Time.now instead of Time.current. They look identical, but they're not.
Time.now returns the actual system time of the server in UTC, completely ignoring config.time_zone. Time.current returns the current time in the application's configured time zone.
# config/application.rb: config.time_zone = 'Pacific Time (US & Canada)'
Time.now
# => 2024-01-15 22:30:00 UTC
Time.current
# => 2024-01-15 14:30:00 -0800
# They represent the same moment, but Time.current respects config.time_zoneIn a Rails application, you should almost always use Time.current. It respects your application's time zone configuration, it plays nicely with Rails' time zone helpers, and it's consistent with how Rails treats timestamps everywhere else.
Time.now is fine for performance metrics (where you want raw system time) or low-level timing, but for business logic, comparisons, and any timestamp that goes into the database, use Time.current.
Here's a real-world pattern from a trading platform: you want to check whether the market is open. Market open times are defined in New York time, but your servers might be anywhere:
class Market
MARKET_OPENS_AT = '09:30'.freeze
MARKET_CLOSES_AT = '16:00'.freeze
MARKET_TZ = 'Eastern Time (US & Canada)'.freeze
def self.currently_open?
now_in_market_tz = Time.current.in_time_zone(MARKET_TZ)
current_time = now_in_market_tz.strftime('%H:%M')
current_time >= MARKET_OPENS_AT && current_time < MARKET_CLOSES_AT
end
end
# This works correctly regardless of where your server is deployed
# because Time.current respects the application's awareness of zonesCompare that to using Time.now: if your servers are in UTC and your market times are in Eastern, the comparison breaks.
Per-user time zones with around_action
In a multi-region application, you'll often need different users to see times in their own local zones. A user in Tokyo should see 9 PM when a user in San Francisco sees 4 AM—same moment in time, different displays.
The pattern for this is to store each user's preferred time zone in the database, then temporarily override config.time_zone during request handling using Rails' Time.use_zone with an around_action callback.
First, add a time_zone column to your users table:
# db/migrate/[timestamp]_add_time_zone_to_users.rb
class AddTimeZoneToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :time_zone, :string, default: 'UTC'
end
endThen set up the around_action in your application controller:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :set_time_zone, if: :current_user
private
def set_time_zone
Time.use_zone(current_user.time_zone) { yield }
end
endNow, every request for an authenticated user temporarily switches the application's time zone. All calls to Time.current, time displays in views, and timestamp formatting will respect the user's zone:
# In a controller action
def show_booking
@booking = Booking.find(params[:id])
# @booking.reserved_at is loaded from the database in UTC,
# but @booking.reserved_at automatically displays in current_user.time_zone
# because the around_action has set Time.use_zone
end
# In the view
<p>Reserved at: <%= @booking.reserved_at.strftime('%I:%M %p') %></p>
# This renders in the user's local timeLet's say a user has time_zone = 'Asia/Tokyo'. A booking created at 2024-01-15 00:30:00 UTC will display as 2024-01-15 09:30:00 to this user during their request.
One nuance: the around_action only affects the current request. If you're running background jobs, you need to handle time zones explicitly there, since there's no authenticated user:
# app/jobs/send_daily_digest_job.rb
class SendDailyDigestJob < ApplicationJob
def perform(user)
# Background jobs don't have around_action, so set the zone explicitly
Time.use_zone(user.time_zone) do
digest_time = Time.current
events = user.events.where(created_at: digest_time.beginning_of_day..digest_time.end_of_day)
UserMailer.daily_digest(user, events).deliver_later
end
end
endCommon mistakes
Storing times in anything other than UTC: Some teams store local times in the database "because it's faster" or "easier to read." This breaks the moment you scale across time zones, and it makes comparisons and sorting unreliable. Never do this.
Mixing Time.now and Time.current: A codebase that uses both is a minefield. Pick Time.current and stick to it for business logic. If you see Time.now in a code review, flag it.
Forgetting about daylight saving time: Don't try to handle DST manually. Rails' time zone objects (ActiveSupport::TimeZone) handle it correctly. If you're doing string comparisons on times without using proper time zone objects, you'll get DST wrong.
Changing config.time_zone dynamically: Some teams think they can change config.time_zone per-request instead of using around_action and Time.use_zone. This leaks time zone state across requests and threads. Always use Time.use_zone for request-scoped changes.
Assuming the database timezone matters: It doesn't. PostgreSQL, MySQL, and others will store your timestamp columns in UTC regardless of their server configuration. Focus on your application layer.
Key takeaways
- Always store timestamps in UTC in the database—this is the foundation everything else builds on.
- Use
config.time_zoneto set a sensible default for your application's logs and debugging, but don't rely on it for business logic. - Use
Time.currenteverywhere in your application, neverTime.now, because it respects your time zone configuration. - For multi-region applications, store each user's
time_zonepreference and usearound_actionwithTime.use_zoneto apply it during request handling. - In background jobs, explicitly call
Time.use_zonewith the appropriate zone, since there's no request context or around_action to handle it. - Trust Rails' time zone conversion, test it occasionally by querying the database directly, and never try to handle DST or timezone math manually.






Comments
Loading comments…