This website uses cookies

Read our Privacy policy and Terms of use for more information.

Overview: Why Timezones Matter

Hey there! Dani here from Lightning Rails, a Rails starter kit to help you launch in weeks, not months. I have recently been working on an app that handles critical logic through time zones. If you have worked with Rails + Timezones in the past, you know it can be a nightmare, but it’s actually very straightforward once you have the correct setup.

Let’s imagine a user in Tokyo creates a task at 2:00 PM local time, but your server (running in UTC) stores it as 5:00 AM. Now a user in New York sees that task at 5:00 AM, not the intended local time.

This isn’t just a display bug; it impacts business logic, user trust, and critical operations like:

  • Showing “today’s” tasks

  • Scheduling notifications

  • Calculating “end-of-day” reports

Timezone bugs are sneaky. And unless you handle them consistently across your entire stack, they’ll bite you.

The Solution: A Layered Timezone Strategy

After refactoring timezone handling in production apps (including ones built on Lightning Rails), here’s a battle-tested, layered approach that works across:

  • Controllers

  • Services

  • Views

  • Background jobs

  • Tests

Let’s break it down.

1. User Timezone Storage

First, store the user's timezone in the database:

# db/migrate/xxxx_add_timezone_to_users.rb
add_column :users, :timezone, :string, default: 'UTC'
# app/models/user.rb
validates :timezone, inclusion: ActiveSupport::TimeZone.all.map(&:name)

💡 Lightning Rails Tip: This also works for parent models, like stores, companies, agencies etc…

2. Core Timezone Helpers

Centralize timezone logic in a helper module:

# app/lib/timezone_helper.rb
module TimezoneHelper
  def in_time_zone(user, &block)
    Time.use_zone(user.timezone || 'UTC', &block)
  end
end

Now you can wrap any logic block in the user’s timezone context. This keeps your code clean and intention clear.

3. Controller Layer: Auto Timezone Context

Use a controller concern to automatically apply the user's timezone:

# app/controllers/concerns/timezone_context.rb
module TimezoneContext
  extend ActiveSupport::Concern

  included do
    around_action :set_time_zone
  end

  private

  def set_time_zone(&block)
    Time.use_zone(current_user.timezone || 'UTC', &block)
  end
end

Include it in your ApplicationController:

# app/controllers/application_controller.rb
include TimezoneContext

Now every controller action automatically runs in the user’s timezone. This makes Time.current and Date.current return the correct local time without manual conversion.

4. Service Layer: Timezone-Aware Services

To avoid leaking timezone logic everywhere, make your service objects timezone-aware:

# app/services/base_service.rb
class BaseService
  include TimezoneHelper

  def with_user_time(user, &block)
    in_time_zone(user, &block)
  end
end

Usage:

with_user_time(user) do
  # All time-based operations here run in user.timezone
  Task.where("due_at >= ?", Time.current.beginning_of_day)
end

5. View Layer: Be Explicit

In views, always render time explicitly in the correct timezone:

<%= task.due_at.in_time_zone(current_user.timezone).strftime("%B %d, %Y %l:%M %p") %>

Avoid relying on Rails’ local_time helper unless you're sure it handles the user’s timezone (not just the browser).

6. Background Jobs: Inject Timezone Context

Jobs usually run in UTC. Wrap logic in a timezone block:

# app/jobs/send_reminder_job.rb
class SendReminderJob < ApplicationJob
  include TimezoneHelper

  def perform(user_id)
    user = User.find(user_id)
    in_time_zone(user) do
      # Do timezone-dependent stuff here
      ReminderService.new(user).send_reminders
    end
  end
end

🛠 With Lightning Rails’ default job and service structure, this pattern integrates smoothly into background workflows.

7. Testing Timezone Logic

Use RSpec’s Time.use_zone to simulate timezones in tests:

it "sends reminders at the correct local time" do
  Time.use_zone("Asia/Tokyo") do
    # Set up task and assert logic runs in Tokyo time
  end
end

Write tests for multiple timezones—especially edge cases like:

  • Daylight saving time shifts

  • End of day / start of day logic

  • Time comparisons across zones

8. Common Pitfalls to Avoid

Pitfall

Solution

Mixing UTC and local times in queries

Wrap logic in timezone context

Assuming controller timezone in helpers

Use explicit in_time_zone calls

Running jobs in UTC without conversion

Wrap jobs in Time.use_zone

9. Advanced Patterns

  • Timezone-Aware Scopes:

scope :today_for, ->(user) {
  in_time_zone(user.timezone) do
    where(created_at: Time.current.all_day)
  end
}
  • Validations: Ensure times are in the future from user’s perspective

  • Caching: Include timezone in cache keys for user-specific data

10. Performance Considerations

  • Minimize conversions: Don’t call .in_time_zone repeatedly

  • Use SQL where possible: PostgreSQL supports timezone-aware queries (AT TIME ZONE)

  • Index UTC timestamps: Always store and index times in UTC for consistency

11. Debugging Timezone Issues

  • Use Time.current.zone to confirm the current context

  • Print Time.current and Time.zone in logs

  • In the Rails console:

    Time.use_zone("Asia/Tokyo") { Time.current }
    

12. Migrating an Existing App

  1. Add timezone column to users

  2. Default to 'UTC' if unknown

  3. Backfill timezones based on IPs or user preferences

  4. Update all time-sensitive logic to use timezone context

  5. Write tests before refactoring!

Conclusion

Timezone handling in Rails isn't a single-line fix—it's a systemic concern. But with a consistent, layered approach like this one (fully compatible with Lightning Rails), you can:

Provide a reliable local-time experience
Ensure correctness in business logic
Avoid timezone bugs before they happen

Key Takeaways

  • Layered handling: Controllers, services, views, and jobs all require tailored timezone strategies

  • Be explicit: Don’t assume timezone context unless you’ve set it

  • Test across zones: Timezone bugs often show up only in production

  • Minimize conversions: Store in UTC, display in user time

Thanks for reading! As always, Happy Building!

Dani

Want this timezone setup pre-wired?
Lightning Rails includes the timezone context boilerplate, helpers, and service patterns described here, ready to go out of the box. 💡

Reply

Avatar

or to participate

Keep reading