Skip to content

Commit 3b2e0bf

Browse files
committed
Timezone bug post
1 parent 3761bbc commit 3b2e0bf

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
created_at: 2026-02-05 13:02:35 +0100
3+
author: Szymon Fiedler
4+
tags: [ruby, rails, time, timezone]
5+
publish: false
6+
---
7+
8+
# The timezone bug that hid in plain sight for months
9+
10+
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.
11+
12+
<!-- more -->
13+
14+
## The broken code
15+
16+
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.
17+
18+
```ruby
19+
def self.date_to_utc(value, timezone_key)
20+
offset = Time.now.in_time_zone(TIMEZONE_MAP.fetch(timezone_key)).formatted_offset
21+
Time.new(*value.to_s.split('-').map(&:to_i), 0, 0, 0, offset).utc
22+
end
23+
```
24+
25+
Looks reasonable, right? Get the timezone offset, create a `Time` object, convert to UTC.
26+
27+
The problem: `Time.now.in_time_zone().formatted_offset` gets the offset for **right now**, then applies it to any date being converted.
28+
29+
## Why this breaks
30+
31+
Run this in December (EST, UTC-5):
32+
33+
```ruby
34+
date_to_utc(Date.new(2023, 6, 20), :eastern)
35+
# Gets -05:00 offset, but June 20 should be EDT (-04:00)
36+
# Result: off by one hour
37+
```
38+
39+
Run the same code in June (EDT, UTC-4):
40+
41+
```ruby
42+
date_to_utc(Date.new(2023, 6, 20), :eastern)
43+
# Gets -04:00 offset, correct for June
44+
# Result: works fine
45+
```
46+
47+
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.
48+
49+
## The fix
50+
51+
```ruby
52+
def self.date_to_utc(value, timezone_key)
53+
tz = ActiveSupport::TimeZone[TIMEZONE_MAP.fetch(timezone_key)]
54+
tz.local(value.year, value.month, value.day, 0, 0, 0).utc
55+
end
56+
```
57+
58+
`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.
59+
60+
## The test that exposed it
61+
62+
Before touching the implementation, I wrote a test to confirm my suspicion — and it failed immediately.
63+
64+
```ruby
65+
it 'produces consistent results regardless of system timezone' do
66+
date = Date.new(2023, 6, 20)
67+
expected = Time.new(2023, 6, 20, 4, 0, 0, 'UTC')
68+
69+
%w[UTC Asia/Tokyo America/Los_Angeles].each do |tz|
70+
Time.use_zone(tz) do
71+
expect(described_class.date_to_utc(date, :eastern)).to eq(expected)
72+
end
73+
end
74+
end
75+
```
76+
77+
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.
78+
79+
## Impact
80+
81+
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.
82+
83+
## Lessons
84+
1. Never use `Time.now` for calculations on other dates. If you need timezone info for a specific date, use that date.
85+
2. Test with explicit timezone manipulation. Don't rely on your system's timezone matching production.
86+
3. DST transitions are sneaky. A bug that manifests only during certain months can survive code review and testing.
87+
4. Know your tools: [`ActiveSupport::TimeZone`](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html)
88+

0 commit comments

Comments
 (0)