Join our FREE personalized newsletter for news, trends, and insights that matter to everyone in America

Newsletter
New

We're Inside The Dst Gap Right Now — Your Code Might Not Be

Card image cap

A field guide for developers building apps that dare to cross meridians

You decided to build an app that tracks events worldwide. Bold move. Now let's talk about the moment you realize that time is not a simple integer and your clever Date.now() will absolutely betray you at the worst possible moment.

Welcome to timezone hell. Population: every developer who ever shipped a scheduling feature.

⚠️ Real-time relevance: I'm writing this on March 24, 2026 — and we're currently living inside the US–Europe DST gap window. The US switched to EDT on March 8, but Europe doesn't switch to CEST until March 29. If your app hardcodes timezone offsets between New York and London (or Prague, or Paris), it's wrong right now.

Step 1 — Know Where You Are (Spoiler: It Doesn't Matter)

You live somewhere. You know your timezone. Congratulations, that's completely irrelevant to your backend.

Your server doesn't care about Prague. Your server speaks UTC.

GMT vs UTC: GMT is a timezone. UTC is a time standard. They happen to share the same offset (+00:00), but GMT observes DST in some edge cases and is rooted in astronomical observation. UTC is atomic-clock precise and DST-free. Always store UTC. Never argue about this.

// Server Side - Kotlin  
 
// ❌ Don't do this 
val now = LocalDateTime.now() // Whose now? YOUR now? Server's now? Tokyo's now?  
 
// ✅ Do this 
val now = Instant.now() // Universal. Unambiguous. Boring in the best way. 

Step 2 — Know Where the Event Is

Your app cares about Tokyo Stock Exchange opening at 09:00 JST That's Tokyo's clock. Not yours.

First: get the canonical event time in its home timezone.

// Server Side - Kotlin 
 
val tokyoZone = ZoneId.of("Asia/Tokyo") 
val tokyoOpen = ZonedDateTime.of( 
    LocalDate.now(tokyoZone), 
    LocalTime.of(9, 0), 
    tokyoZone 
) 
// tokyoOpen = 2026-03-24T09:00+09:00[Asia/Tokyo] 

Now convert to UTC so you have a real anchor point:

// Server Side - Kotlin 
 
val tokyoOpenUtc = tokyoOpen.toInstant() 
// 2026-03-24T00:00:00Z  ← this is your ground truth 

Good. Now you have something you can actually compute with.

Step 3 — The Countdown Is Just Subtraction (Or Is It?)

// Server Side - Kotlin 
 
val countdown = Duration.between(Instant.now(), tokyoOpenUtc) 
println("Market opens in: ${countdown.toMinutes()} minutes") 

Easy, right? Ship it.

...

Wait.

Step 4 — Did You Think About DST?

Japan doesn't observe DST. Lucky them. But your users might live somewhere that does.

Here's the trap: your countdown is correct (you're comparing Instant values — UTC under the hood). But the displayed local time on the client side may shift by ±1 hour depending on the season.

// Client side - TypeScript 
 
const winterTs = new Date("2026-01-15T00:00:00Z"); 
winterTs.toLocaleString(undefined, { timeZoneName: "short" }); 
// Prague: "15. 1. 2026, 1:00:00 CET"   ← UTC+1, winter time 
 
const summerTs = new Date("2026-06-01T00:00:00Z"); 
summerTs.toLocaleString(undefined, { timeZoneName: "short" }); 
// Prague: "1. 6. 2026, 2:00:00 CEST"   ← UTC+2, summer time !  

Same code. Different timestamp. Different hour. The browser handles the rule — but only if you let it. The moment you hardcode +01:00 you're frozen in January forever.

Use ZoneId, not ZoneOffset. One knows about DST. The other does not.

// Server Side - Kotlin 
 
// ❌ Hardcoded offset - breaks in summer/winter 
val prague = ZoneOffset.of("+01:00") 
 
// ✅ Named zone - handles DST transitions automatically 
val prague = ZoneId.of("Europe/Prague") 

Step 4.5 — The DST Overlap Problem (This Is Where It Gets Spicy) ????

Here's what most tutorials skip: different regions switch DST on different dates.
And that gap between switches is where hardcoded timezone differences silently break.

The US–Europe Gap Window (happening right now)

As of this writing — late March 2026 — we are living inside exactly this trap:

  • ???????? US switched to EDT on March 8 (second Sunday of March)
  • ???????? Europe switches to CEST on March 29 (last Sunday of March)

That's 3 weeks where the US–Europe offset is not the usual value:

Period                  | New York (ET) | Prague (CET/CEST) | Difference 
------------------------|---------------|-------------------|------------ 
Winter (before Mar 8)   | EST  = UTC-5  | CET  = UTC+1      | 6 hours 
Gap window (Mar 8–28)   | EDT  = UTC-4  | CET  = UTC+1      | 5 hours???? 
Summer (after Mar 29)   | EDT  = UTC-4  | CEST = UTC+2      | 6 hours 

If you hardcoded "NYSE opens 3:30pm Prague time" — you're wrong for 3 weeks every spring and 3 weeks every autumn. Congratulations, your bug has a season.

// Server Side - Kotlin 
 
// ❌ This is wrong 6 weeks per year 
val nyseOpenInPrague = LocalTime.of(15, 30) // "I know the offset is 6h" 
 
// ✅ Let the library do the calendar math 
val nyseOpen = ZonedDateTime.of( 
    LocalDate.now(ZoneId.of("America/New_York")), 
    LocalTime.of(9, 30), 
    ZoneId.of("America/New_York") 
) 
val pragueView = nyseOpen.withZoneSameInstant(ZoneId.of("Europe/Prague")) 
println("NYSE opens at: ${pragueView.toLocalTime()} Prague time") 
// Correctly prints 15:30 in winter, 15:30 in summer, and 14:30 in the gap ???? 

The Australia Wildcard — When the Hemispheres Collide

Australia is on the opposite DST schedule because, well, opposite hemisphere. The ASX (Sydney) observes:

  • AEDT (UTC+11) — October through April (their summer)
  • AEST (UTC+10) — April through October (their winter)

This creates a beautiful four-state matrix with Europe alone:

Period               | Sydney        | London (GMT/BST) | Difference 
---------------------|---------------|------------------|------------ 
Jan (EU winter,      | AEDT = UTC+11 | GMT  = UTC+0     | 11 hours 
 AU summer)          |               |                  | 
Apr transition week  | AEST = UTC+10 | BST  = UTC+1     | 9 hours ???? 
Jul (EU summer,      | AEST = UTC+10 | BST  = UTC+1     | 9 hours 
 AU winter)          |               |                  | 
Oct transition week  | AEDT = UTC+11 | BST  = UTC+1     | 10 hours ???? 

The Sydney–London offset swings between 9 and 11 hours across the year, with two transition windows where it's a completely different value than either stable state.

Any app that displays "London time" relative to "Sydney time" from a hardcoded diff will be wrong four times per year, at transitions in both directions.

The only correct approach:

// Client Side - TypeScript 
 
function getOffsetBetween(zone1: string, zone2: string, at: Date): number { 
  // Never hardcode. Always compute. Timezone rules do the rest. 
  const fmt = (tz: string) => 
    new Intl.DateTimeFormat("en", { timeZone: tz, timeZoneName: "shortOffset" }) 
      .formatToParts(at) 
      .find(p => p.type === "timeZoneName")?.value ?? ""; 
 
  const parseOffset = (s: string) => { 
    const m = s.match(/GMT([+-])(\d+)(?::(\d+))?/); 
    if (!m) return 0; 
    return (m[1] === "+" ? 1 : -1) * (parseInt(m[2]) * 60 + parseInt(m[3] ?? "0")); 
  }; 
 
  return parseOffset(fmt(zone1)) - parseOffset(fmt(zone2)); 
} 
 
// Usage 
const now = new Date(); 
console.log(getOffsetBetween("Australia/Sydney", "Europe/London", now)); 
// Returns the correct value today, tomorrow, and in October 

The Summary Rule

If you have ever typed a number of hours as a timezone difference between two places — you have a bug. You just don't know which week it will appear yet.

Step 5 — Did You Consider the Date?

Tokyo opens Monday 09:00 JST. You're in New York on Sunday evening. The event is
tomorrow in Tokyo and today in your database query.

// Server Side - Kotlin 
 
val tokyoZone = ZoneId.of("Asia/Tokyo") 
val nyZone    = ZoneId.of("America/New_York") 
 
val nowInTokyo = ZonedDateTime.now(tokyoZone)  // Monday 
val nowInNY    = ZonedDateTime.now(nyZone)      // Sunday 
 
// Same Instant. Different dates. Different days of the week. 
// Your "today's events" filter just silently excluded Tokyo. 

Rule: Always derive local dates from the event's timezone, not from your server's LocalDate.now().

Step 6 — Architecture: Server/Client Contract

????️ Here's the pattern that keeps you sane across the whole stack:

┌──────────────────────────────────────────────────────┐ 
│  SERVER                                              │ 
│  • Stores everything as UTC (Instant / epoch ms)    │ 
│  • Stores user's IANA timezone string alongside     │ 
│    user-facing timestamps                           │ 
│  • Never stores offsets (+01:00) — they're seasonal │ 
└───────────────────┬──────────────────────────────────┘ 
                    │ JSON: { "openTime": "2026-03-24T00:00:00Z", 
                    │         "timezone": "Asia/Tokyo" } 
┌───────────────────▼──────────────────────────────────┐ 
│  CLIENT                                              │ 
│  • Receives UTC timestamps                          │ 
│  • Renders in user's local timezone (browser API)   │ 
│  • Sends edits back as UTC or with explicit tz info │ 
└──────────────────────────────────────────────────────┘ 
// Client Side - TypeScript 
 
// Server response contract 
interface MarketEvent { 
  openTimeUtc: string;    // ISO-8601 UTC: "2026-03-24T00:00:00Z" 
  marketTimezone: string; // IANA: "Asia/Tokyo" 
} 
 
// Client rendering 
function formatEventTime(event: MarketEvent, displayZone: string): string { 
  return new Intl.DateTimeFormat("en-US", { 
    timeZone: displayZone, 
    hour: "2-digit", 
    minute: "2-digit", 
    timeZoneName: "short", 
  }).format(new Date(event.openTimeUtc)); 
} 

Step 7 — What About the Cloud ☁️ ?

"My app runs on three continents. Does my replica in Singapore care about Prague time?"

No. And it shouldn't.

Your databases store UTC-based timestamps:

  • Firestore / MongoDB → epoch-based UTC values (timezone is not stored at all)
  • PostgreSQL TIMESTAMPTZ → stored as UTC internally, converted on read/write

Timezones are not a storage problem.
They are a scheduling and interpretation problem.

And sometimes — they are your business logic.

The only time you should actually care about timezones in your backend:

  • Sharding data by date (whose “today”? see Step 5)
  • Log correlation across multi-region deployments
  • Scheduled jobs that must fire at "9am local" per region
  • Business rules tied to real-world time (markets, bookings, SLAs)

This is where many systems break.

Because this is NOT display logic — this is domain logic.

If your app says:

  • "Send email at 9:00"
  • "Market opens at 09:30"
  • "Booking starts at midnight"

That time belongs to a specific timezone, and must be modeled explicitly.

// Server Side - Kotlin 
 
// ❌ Ambiguous — whose 9:00? 
LocalTime.of(9, 0) 
 
// ✅ Explicit — tied to real-world meaning 
ZonedDateTime.of(date, LocalTime.of(9, 0), ZoneId.of("America/New_York")) 

Never rely on your server region timezone (e.g. AWS region). It is irrelevant to your application logic.

For everything else: UTC in, UTC out, convert at the edges.

The Mental Model

Event happens in the real world 
        ↓ 
Expressed in event's local timezone  (09:00 JST) 
        ↓ 
Stored as UTC on your server         (00:00 UTC) 
        ↓ 
Transmitted as UTC over the wire 
        ↓ 
Displayed in user's local timezone   (browser/client) 

???? That's it. The whole game. Fight anyone who breaks this pipeline.

???? Bonus: Real Lessons from Building TradeDialer

I built TradeDialer, a global market hours tracker covering 25 exchanges with live countdowns and index data.

Here's what actually hurt:

1. Countdown wording is a UX problem, not just a math problem

A raw duration like "opens in 61 hours" is technically correct and practically useless on a Friday afternoon. The countdown needs contextual language:

Opens in 2h 34m          ← same day, simple case 
Opens Monday 09:30       ← weekend ahead 
Opens in 3 days          ← holiday closure 

Every one of those branches must be computed in the exchange's local timezone, not the server's. "Monday" in Tokyo is not "Monday" in New York.

2. Status thresholds need their own timezone logic

TradeDialer uses a four-state color system to communicate urgency at a glance:

Color Meaning
???? Green Market is open
???? Amber Closing in < 1 hour
???? Blue Opening in < 1 hour
⚫ Gray Closed (weekend / holiday / outside hours)

The amber and blue thresholds are computed as a diff against the exchange's session end/start — as ZonedDateTime, not a raw UTC comparison. Otherwise the "closing soon" window misfires around DST transitions.

3. Live data fallback is a timezone-triggered state machine

When US markets close, direct exchange feeds dry up. TradeDialer automatically switches to a Yahoo Finance fallback for foreign markets that are still open — and surfaces the source change with a visible badge so users always know what they're looking at.

The trigger for that switch? Computed using exchange timezone + session hours. Not UTC midnight. Not server time. The exchange's own clock.

4. Holiday calendars are a maintenance tax, not a one-time task

Holiday data looks static — until a government announces a surprise market closure three days before it happens (this is a real thing that happens). Building a market hours app means committing to keeping holiday data fresh across 25 exchange calendars, globally.

Design your holiday config as versioned and hot-reloadable, not hardcoded constants. You will need to push an update on short notice someday.

5. Scope decisions must be explicit, not accidental

NYSE pre-market starts at 04:00 EST. NYSE after-hours runs until 20:00 EST. TradeDialer deliberately excludes extended hours and shows only regular session data.

"We don't show extended hours" is a product decision.
"Extended hours are broken" is a bug.
Know which one you're shipping.

TL;DR — The Timezone Survival Kit

Rule Why
Store as Instant / UTC epoch No offset confusion
Use IANA zone IDs, not offsets DST is handled for you
Never hardcode hour differences between zones The gap window will find you
Derive dates per-timezone Date line is real
Convert only at display time Single source of truth
Evaluate holidays in exchange timezone Local days, not UTC days
Model data quality explicitly Trust is a feature
Make holiday data hot-reloadable Governments are unpredictable
Never trust LocalDateTime across systems It's a lie with no context

Built while debugging why TradeDialer showed Tokyo as opening at 1am. It was. In Prague. In December. Nobody told the server.

Check it out: trade-dialer.com