Three fields in a WeatherAPI response all seem to answer the same question — is it currently light outside? — and all three can disagree with each other at the same time. is_day in the current block, sunrise and sunset in the astronomy block, and the condition icon path (which encodes day/night in the filename) are computed separately, against slightly different inputs, and are correct in slightly different senses.
If you’re building anything that depends on distinguishing daylight from darkness — adjusting UI themes, controlling smart lighting integrations, filtering condition icons correctly, or gating notifications — this is worth understanding before you hit it in production rather than after.
What is_day Actually Is
is_day is a binary integer (1 or 0) attached to the current conditions block. It reflects the state at the moment the observation was assembled — meaning it’s tied to the observation time, not your request time, and not the location’s civil twilight definition. The observation is sourced from the nearest METAR station or model output, whichever is more current, and the day/night flag follows from that source’s timestamp and the computed solar position for the location’s coordinates.
That last part matters: solar position, not clock time. Civil twilight, nautical twilight, astronomical twilight — those are all different thresholds that jurisdictions and use cases apply on top of raw sunrise/sunset. is_day uses a basic horizon-crossing calculation. Sun above the horizon: 1. Below it: 0.
So a request made at 07:45 local time might return is_day: 1 even though your client-side calculation of sunrise says 07:52. Or vice versa. This isn’t a bug. It’s two different things measured with two slightly different methods.
What sunrise and sunset Actually Are
The astronomy endpoint (and the astronomy block embedded in forecast responses) returns sunrise and sunset as time strings formatted in the location’s local time, not UTC. The underlying calculation is a standard solar position formula using the location’s latitude/longitude and date — specifically, the moment the upper limb of the sun crosses the observer’s horizon at sea level, which is the conventional definition used by agencies like NOAA and the UK Met Office.
Two things make these strings misleading in practice.
First, they don’t account for terrain. A location at the base of a west-facing ridge will lose direct sunlight noticeably earlier than the formula says. High-latitude mountain valleys in winter can go dark hours before the astronomical calculation suggests. There’s no field for this — no weather API resolves topographic horizon shading without additional elevation model work on your end.
Second, the time strings come back in local time with no timezone offset marker. They look like "06:43 AM". If you’re parsing these on a backend running in UTC, or comparing them against epoch timestamps from another field, you need the location’s UTC offset to make the comparison meaningful. The localtime and tz_id fields from the same response are what anchor these — but you have to do that joining yourself.
The Icon Filename Problem
This one catches people. The condition icon URL encodes day or night in the path — something like /day/116.png vs /night/116.png. The icon is selected based on is_day at response generation time. But if you’re building a forecast display showing tomorrow’s hourly conditions, the is_day flag on each hour is computed independently for that hour — meaning you can’t use the top-level current is_day to select icons for forecast hours. Each hourly block carries its own is_day, and that’s the one to use.
Where things go wrong: pulling the icon URL from the hourly block correctly, then trying to override it by recomputing day/night client-side from sunrise/sunset string parsing — and hitting the edge cases (mismatched timezones, AM/PM string handling, DST gaps) that make that harder than expected. The simpler path is trusting the is_day on each hourly object and not second-guessing it.
When They Disagree and What to Do About It
The most common divergence: the current block says is_day: 1 but you compute from the astronomy strings that it’s after sunset. This happens in two scenarios.
Caching delay. If you cached a response at 19:50 and sunset was at 20:05, then re-serve that cached response at 20:12, is_day is stale. The sunrise/sunset strings are still accurate (they don’t change within a day), but the flag is now wrong. This is an argument for not caching current-conditions responses across the day/night boundary without invalidation logic — or at minimum checking whether your cached response’s last_updated_epoch is more than a few minutes old before using its is_day for rendering decisions.
Polar and near-polar locations. At high latitudes in summer or winter, the solar geometry gets strange. Locations above the Arctic Circle can have sunrise and sunset return as edge-case strings (or missing entirely in some implementations) while is_day correctly reflects 20 straight hours of daylight. Don’t build logic that assumes sunrise always precedes sunset on the same calendar day. Above roughly 66°N or below 66°S, that assumption fails for weeks at a time.
Which Field to Use for What
For real-time rendering decisions (current icon, current UI theme, “is it light right now”): use is_day from the current block. It’s the most direct answer and doesn’t require string parsing.
For forecast rendering across hourly blocks: use is_day from each individual hourly object. Don’t interpolate from sunrise/sunset strings across hours yourself — it’s fragile and unnecessary when the flag is already there per hour.
For business logic that cares about the actual moment of sunrise or sunset — scheduling tasks, generating reports, building a golden-hour photography feature — use the astronomy block values, but anchor them against tz_id before comparing to any UTC-based timestamps.
For anything polar, high-latitude, or mountainous: test explicitly with coordinates in those regions. Tromsø, Norway (69.6°N) in late June will stress-test your assumptions about sunrise/sunset in ways that London or Chicago never will.
A Concrete Example Worth Stepping Through
Say you’re building a smart home integration that triggers exterior lights when is_day flips to 0. You’re caching responses on a 15-minute interval. Sunset is at 20:47. Your 20:30 fetch returns is_day: 1. Your 20:45 fetch still returns 1 — sun hasn’t crossed the horizon yet. Your 21:00 fetch returns 0. Lights trigger.
Twelve minutes of delay is acceptable for most use cases. It isn’t if you’re billing by lighting state or syncing with something else running on astronomical time. The fix isn’t to swap in sunrise/sunset string math — it’s to shorten the polling interval around known transition times, which you can compute in advance from the astronomy block for the current day.
Fetch the day’s astronomy data once at midnight local time, parse sunrise and sunset with proper timezone handling, then tighten your polling window to every 2-3 minutes in the 10 minutes bracketing each transition. Outside those windows, 15-minute caching is fine.
These fields aren’t redundantly encoding the same value — they’re measuring the same physical phenomenon from different computational angles, each with its own guarantees and blind spots. The question worth sitting with before you ship: which guarantee does your feature actually need?
