Part of the Pipeline Observability & Alerting guide.
Operative rule: the badge must be computed from the source timestamp — when the data was produced — never from when the build ran, because those two numbers diverge in exactly the failure this badge exists to expose.
What the Badge Is Actually Reporting
There are three timestamps in a rebuild and they mean different things. The build start is when your job woke up. The publish time is when the artifact became visible. The source timestamp is when the underlying data was last genuinely updated by whoever produces it. Only the third one describes what a reader is looking at.
The gap between them is the entire subject. A pipeline can run perfectly on schedule for a fortnight while its upstream export is frozen; every build is green, every publish succeeds, and the map has been wrong for two weeks. A badge fed from the build time reports “updated 4 minutes ago” throughout. A badge fed from the source timestamp reports the truth from the first morning.
Production-Ready Implementation
The build writes an absolute instant into the manifest; the page turns it into a relative age and keeps it current.
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
def source_timestamp_from(payload: dict, fallback_header: str | None) -> datetime:
"""Prefer the source's own field; fall back to its Last-Modified header."""
raw = payload.get("generated_at") or payload.get("updated_at")
if raw:
return datetime.fromisoformat(str(raw).replace("Z", "+00:00")).astimezone(
timezone.utc
)
if fallback_header:
from email.utils import parsedate_to_datetime
return parsedate_to_datetime(fallback_header).astimezone(timezone.utc)
raise ValueError(
"no source timestamp available — treat this as a data-quality defect, "
"not as a reason to stamp the build time"
)
def write_manifest(out: Path, source_ts: datetime, run_id: str,
warn_hours: int = 30, critical_hours: int = 72) -> None:
out.write_text(
json.dumps(
{
"source_timestamp": source_ts.isoformat(),
"run_id": run_id,
"thresholds": {"warn_hours": warn_hours,
"critical_hours": critical_hours},
},
indent=2,
),
encoding="utf-8",
)
const UNITS = [
["year", 31536000], ["month", 2592000], ["day", 86400],
["hour", 3600], ["minute", 60],
];
function relativeAge(seconds) {
for (const [unit, size] of UNITS) {
if (seconds >= size) {
const n = Math.floor(seconds / size);
return `${n} ${unit}${n === 1 ? "" : "s"} ago`;
}
}
return "just now";
}
export async function mountFreshnessBadge(el, manifestUrl) {
const manifest = await fetch(manifestUrl, { cache: "no-cache" }).then((r) => r.json());
const source = new Date(manifest.source_timestamp);
const { warn_hours: warn, critical_hours: critical } = manifest.thresholds;
function paint() {
const ageSeconds = (Date.now() - source.getTime()) / 1000;
const hours = ageSeconds / 3600;
const state = hours >= critical ? "critical" : hours >= warn ? "warn" : "ok";
el.dataset.state = state; // CSS decides how each state looks
el.querySelector(".age").textContent = relativeAge(ageSeconds);
el.querySelector(".absolute").textContent = source.toISOString().slice(0, 16)
.replace("T", " ") + " UTC";
el.setAttribute("aria-label", `Data as of ${source.toUTCString()} — ${state}`);
}
paint();
setInterval(paint, 60000); // stays honest on a page left open
}
Fetching the manifest with no-cache is deliberate: it is the one object in the system that must never be stale, because a cached manifest would report a freshness that is itself out of date. Everything else — tiles, style, data files — can and should be cached aggressively, as covered in Cache Invalidation Strategies.
Designing the Badge’s States
Two design rules keep the badge useful. It must never rely on colour alone to signal state — the wording changes too, so the meaning survives a monochrome screen or a reader who cannot distinguish the hues. And it must always show the absolute timestamp somewhere, because “9 days ago” is what raises the alarm but “2026-07-27 02:41 UTC” is what goes into the incident ticket.
Where the timestamp comes from, in order of preference
Not every source announces when its data was produced, so the pipeline needs a documented order of preference rather than whatever the first implementation happened to reach for. Each fallback is weaker than the one above it, and the weakest is a defect to raise upstream rather than a solution.
Verification Steps
- Set the manifest’s timestamp to a known past instant and confirm the rendered age matches to the minute.
- Leave the page open past a threshold boundary and confirm the badge changes state without a reload.
- Remove the source timestamp from the upstream payload and confirm the build fails rather than substituting the build time.
- Confirm the manifest is fetched with revalidation while tiles are still served from cache.
- Read the badge with a screen reader and confirm the state is announced in words, not implied by colour.
Common Errors & Fixes
The badge always says “just now”
The pipeline is stamping the manifest with its own clock. Trace where source_timestamp is set; if the upstream provides nothing, use the fetched file’s last-modified header and raise the gap as a data-quality issue with the provider.
The age is wrong by exactly one timezone offset
A naive local timestamp is being compared against a UTC one. Make every timestamp timezone-aware at the boundary where it enters the pipeline, and serialise in ISO 8601 with an offset.
The badge lags behind on a long-lived dashboard
There is no timer, so the age was computed once at load. Repaint on an interval, and also on visibilitychange so a tab restored after a day is immediately correct.
The badge disagrees with the alerting system
They are reading different fields. Both must read the same source_timestamp from the same manifest — a badge that contradicts the on-call alert costs more trust than either one earns.
Wording the Badge for the People Who Read It
The badge’s text is the part that determines whether it changes behaviour, and it is usually written last and least carefully. Three principles make it work.
State the age in the unit the reader thinks in. “Updated 4 hours ago” is immediately meaningful; “updated 14,712 seconds ago” is not, and neither is a bare timestamp for anyone who has not memorised the current time in UTC. Relative wording for the age, absolute for the record.
Say what is stale, not just that something is. A dashboard with three layers refreshed on different cadences needs a badge that either reports the oldest of them explicitly or breaks the age out per source. Reporting a single age that silently means “the freshest layer” is worse than reporting nothing, because it invites confident action on the layer that is actually a week old.
And when the state is critical, say what to do. “This map is out of date — last updated 9 days ago. Check with the operations team before acting on it.” is a complete instruction. “Stale” is a label that leaves the reader to decide what it means, which they will do in whatever direction suits their current task.
There is also a placement question. On a wide screen the badge belongs near the map’s title, where it is read as part of the map’s identity. On a phone, screen space is scarce and the badge competes with the map itself; collapsing it to a small indicator that expands on tap keeps it available without taking a permanent share of a small viewport. What it must not do is scroll out of view — a freshness warning that only exists above the fold is invisible to a reader who arrived by deep link and started panning.
Finally, keep the badge honest when the age is unknown. A missing source timestamp should render as “data age unknown”, never as a comfortable-looking default, because an unknown age is a failure state and should look like one.
The badge is the cheapest piece of observability in the whole pipeline and frequently the most effective, because it puts the one number that matters in front of the people who will act on it rather than in a dashboard only the pipeline’s authors ever open.
Related
- Pipeline Observability & Alerting — the parent guide, including the alert this badge mirrors
- Emitting Structured Run Records from a Python Map Pipeline — where the timestamp is captured
- Cache Invalidation Strategies for Geo-Dashboards — why the manifest is the one object that must not be cached
- Generating a Map Legend That Matches Your Python Colour Ramp — the other place the data vintage belongs