The timezone bug that hid in plain sight for months
… and check why 5600+ Rails engineers read also this
The timezone bug that hid in plain sight for months
We recently fixed a bug in a financial platform’s data sync that had been silently causing inconsistencies for months. The bug was elegant in its simplicity: checking DST status for “now” when converting historical dates.
The broken code
I found this while debugging a different sync issue — the real bug turned out to be hiding in a helper method I wasn’t even looking at.
def self.date_to_utc(value, timezone_key)
offset = Time.now.in_time_zone(TIMEZONE_MAP.fetch(timezone_key)).formatted_offset
Time.new(*value.to_s.split('-').map(&:to_i), 0, 0, 0, offset).utc
end
Looks reasonable, right? Get the timezone offset, create a Time object, convert to UTC.
The problem: Time.now.in_time_zone().formatted_offset gets the offset for right now, then applies it to any date being converted.
Why this breaks
Run this in December (EST, UTC-5):
date_to_utc(Date.new(2023, 6, 20), :eastern)
# Gets -05:00 offset, but June 20 should be EDT (-04:00)
# Result: off by one hour
Run the same code in June (EDT, UTC-4):
date_to_utc(Date.new(2023, 6, 20), :eastern)
# Gets -04:00 offset, correct for June
# Result: works fine
Same input, different output depending on when you run it. Your tests pass in summer, fail in winter. Data syncs would occasionally miss records or pull wrong date ranges, depending on DST periods.
The fix
def self.date_to_utc(value, timezone_key)
tz = ActiveSupport::TimeZone[TIMEZONE_MAP.fetch(timezone_key)]
tz.local(value.year, value.month, value.day, 0, 0, 0).utc
end
ActiveSupport::TimeZone#local handles DST correctly for the specific date being converted. June dates always get EDT, January dates always get EST, regardless of when the code runs.
The test that exposed it
Before touching the implementation, I wrote a test to confirm my suspicion — and it failed immediately.
it 'produces consistent results regardless of system timezone' do
date = Date.new(2023, 6, 20)
expected = Time.new(2023, 6, 20, 4, 0, 0, 'UTC')
%w[UTC Asia/Tokyo America/Los_Angeles].each do |tz|
Time.use_zone(tz) do
expect(described_class.date_to_utc(date, :eastern)).to eq(expected)
end
end
end
This test runs the same conversion in UTC, Tokyo, and LA timezones. The old implementation would produce different results depending on system timezone and time of year.
Impact
We caught this before it caused visible production issues, but the potential impact for a financial data integration was significant: off-by-one-hour shifts during DST transitions could cause missed records in date-range queries and validation mismatches between systems.
Lessons
- Never use
Time.nowfor calculations on other dates. If you need timezone info for a specific date, use that date. - Test with explicit timezone manipulation. Don’t rely on your system’s timezone matching production.
- DST transitions are sneaky. A bug that manifests only during certain months can survive code review and testing.
- Know your tools:
ActiveSupport::TimeZone