Why Your Weather API Timestamps Are Lying to You (And How to Fix It)

Timezone bugs are the silent killers of weather integrations. Your forecast renders the wrong day. Your historical query returns 23 hours of data instead of 24. A flight briefing tool shows a TAF valid time that’s off by one hour during DST. None of these fail loudly — they just silently serve wrong information until a user notices.

The specific problem: WeatherAPI.com (and every other weather data provider) returns timestamps in multiple formats across a single response. Some are Unix epoch integers. Some are ISO 8601 strings with UTC offsets. Some are local-time strings with no offset at all. If you’re not handling each field type explicitly, you’re shipping a timezone bug.

The Three Timestamp Shapes in a Typical Response

Pull a forecast response from /v1/forecast.json and look at what you actually get:

  • location.localtime — a human-readable local time string like "2024-03-15 14:30", no UTC offset, no timezone identifier
  • location.localtime_epoch — Unix timestamp in seconds, UTC
  • forecast.forecastday[].date — a date string in YYYY-MM-DD format, local to the queried location
  • forecast.forecastday[].date_epoch — Unix timestamp representing midnight local time for that date, not UTC midnight
  • hour[].time — local time string again, no offset
  • hour[].time_epoch — Unix timestamp, UTC

date_epoch is the one that catches people badly. A developer sees an epoch field and assumes UTC midnight. It’s not. For a location in AEST (UTC+10), date_epoch for March 15 will be 14 hours behind what they expect. Date-range queries drift. Charting libraries show the wrong day boundary.

The Real-World Failure: Aviation Briefing Apps

Here’s a concrete scenario. You’re building a pre-flight weather briefing tool. A pilot queries conditions for YSSY (Sydney Airport) at 23:00 local time on March 14. Your backend calls the API, gets back forecast data, and needs to decide which forecast day’s hourly data to display.

If you use forecastday[].date and parse it as UTC, you’ll grab March 15’s data when you should be showing March 14’s — because Sydney at 23:00 AEDT is 12:00 UTC, still March 14 in UTC. The date field correctly says "2024-03-14" in local time. The problem surfaces when you convert that date string to a UTC timestamp for comparison: March 14 local midnight in AEDT is March 13 14:00 UTC. Your comparison logic is now completely off.

The fix sounds obvious once you see it: never use date or localtime for UTC comparisons. Use the _epoch variants exclusively for timestamp arithmetic, and reserve string fields for display only.

DST Makes This Worse in Specific Regions

Australia, the US, and most of Europe observe DST, but they don’t change on the same day. Arizona doesn’t observe DST at all. Lord Howe Island uses a 30-minute DST offset, giving it UTC+11 in summer. If your app caches a UTC offset for a location and reuses it later, you will eventually serve wrong data during a DST transition window.

The location.tz_id field in WeatherAPI responses gives you the IANA timezone identifier — "America/New_York", "Australia/Sydney", and so on. Use that. Don’t use location.utc_offset for anything except display. The IANA identifier is what you feed to a proper timezone library: pytz or zoneinfo in Python, date-fns-tz in JavaScript.

A minimal correct pattern in Python:

from zoneinfo import ZoneInfo
from datetime import datetime

tz = ZoneInfo(response["location"]["tz_id"])

# Use epoch for math, localize only for display
for hour in forecast_day["hour"]:
    utc_dt = datetime.utcfromtimestamp(hour["time_epoch"]).replace(tzinfo=ZoneInfo("UTC"))
    local_dt = utc_dt.astimezone(tz)
    print(f"{local_dt.strftime('%H:%M')} — {hour['temp_c']}°C")

Don’t parse hour[].time directly into a datetime object. It carries no offset, so your parser assumes local system time — whatever the server running your code happens to be set to. In a containerized environment that’s usually UTC, which means at 23:00 in Sydney you’re off by 11 hours.

Historical Data Queries and the Midnight Trap

The /v1/history.json endpoint takes a dt parameter as YYYY-MM-DD, interpreted as local to the queried location. If you’re running a nightly cron pipeline in UTC and passing datetime.utcnow().strftime("%Y-%m-%d"), you’ll request the wrong date for any location east of UTC. From 00:00 UTC until that timezone’s local midnight, you’re one day behind.

Convert your pipeline’s UTC execution time to the target location’s local date before building the query string. Cache tz_id per location ID — it doesn’t change often, though governments do occasionally redraw timezone boundaries. Samoa switched sides of the date line in 2011, so it’s not purely theoretical.

Marine and Offshore Use Cases

Marine applications add another layer. Vessels don’t operate in the timezone of their departure port or destination — they run on ship’s time, which might be UTC or might be manually adjusted by the captain. When you query weather for a position at sea using lat/lon, WeatherAPI infers the timezone from the coordinates. Mid-ocean locations often resolve to a timezone that has nothing to do with what the crew is actually using.

For anything offshore: work entirely in UTC epoch values. Don’t trust the inferred timezone for a lat/lon in the middle of the Pacific. Render in UTC on the client and let the application layer handle local conversion if the user needs it.

One More Edge Case Worth Calling Out

The forecast endpoint returns hourly data for up to 3 days, with each forecastday object containing 24 hour entries. During a DST transition, there are either 23 or 25 hours in local time — but the API returns exactly 24 entries regardless. On a 23-hour day (spring forward), one hour disappears from local representation. On a 25-hour day (fall back), one hour is duplicated locally, though both entries carry distinct time_epoch values. If you’re doing gap detection or completeness checks on hourly data, len(hours) == 24 will always pass while the epoch-based time span is actually 23 or 25 hours.

This bug surfaces exactly twice a year, in a timezone-specific subset of your users, and takes a surprisingly long time to diagnose if you don’t already know to look for it.

Scroll to Top