Predicted Traffic Learns When to Trust Recent Roads
The adaptive selector described in the second half of this post stayed an experiment: the June export that used it was built and loaded into Valhalla tiles for testing, and production kept one 180-day half-life for every road. Local road time, the guard for empty windows and the 180-day half-life are what production traffic data uses today.
One edge of a motorway in eastern Germany was driven at about 120 km/h until March 2024. For the next twenty months the GPS traces on it ran at 85 to 95 km/h, and since December 2025 they are back above 120. By 3 June, the last day of our June observations, the edge had 1,503 of them, and their plain median was 99.9 km/h:
Each dot is one traversal, day and night together, faded by how much it counts. Our traffic data gives every road a day speed and a night speed for Valhalla, and for this road the plain median is wrong by more than 20 km/h. This post is about how much an old observation should count, and about the decisions that have to be right before that question even makes sense.
Whose clock
We store every observation with its UTC time, which is the right record: one timestamp, one moment, wherever the road is. The day and night speeds, though, are defined by the clock of the road. Valhalla uses the day speed from 07:00 to 19:00 local time, and Monday 08:00 in Berlin is not Monday 08:00 in New York.
Until May, our day and night windows were cut in UTC. When we converted a sample of 25,000 observations per country to the local time of each road, with daylight saving time from the time zone database, this is how many of them landed in the other window:
Over the whole sample it was 17.6%; in Australia, 92%. Every scalar speed is now computed in local road time: the road's time zone comes from the routing tile it lies in, and an observation without a time zone stops the export instead of being guessed.
Windows without evidence
A road's night speed is its own level times the night-to-day ratio of its cohort, the roads of the same country, region, class and density. A road that was never driven at night would still get a confident night speed from that ratio, so the ratio is now pulled toward 1 when the road's own window has little support:
support = window observations × reliability
weight = support / (support + 5)
night/day used = 1 + weight × (cohort ratio − 1)
With no night observations the road keeps its own level at night; with a few dozen it gets almost the whole cohort ratio.
How long to remember
Freshness weights give every observation the weight 2−age / half-life. With a half-life of 180 days, an observation from a year ago counts a quarter, and one from two years ago a sixteenth. The median above becomes 101.6 km/h: the twenty slow months are still most of the weight. it is 116.1 km/h, and it is 123.6, close to what the road has done since December.
A short memory has a price. With a half-life of 30 days the median rests on an effective 126 observations instead of
1,503, and on a road with few traces it would follow whatever the last three cars did. To limit that, the fresh
estimate is blended back toward the unweighted one by the road's effective support, with the weight
support / (support + 30).
We scored each half-life on four 28-day test periods between November 2025 and March 2026, 20.2 million held-out observations. Against the unweighted local-time model, the mean error per observation went from 9.863 km/h to 9.721 with a 90-day half-life, 9.713 with 180 days and 9.726 with 365. A single global half-life is best at 180 days, and the differences between the three are small because most roads did not change.
Three memories at once
The road above did change, and the changed the other way in March 2026. One half-life cannot be right for both kinds of road and for the quiet majority, so in June we tried a selector that picks one of three for each edge and each window:
- 365 days when the edge has fewer than 10 observations, no observations in that window, or fewer than 96 on a service road or in Russia, Kazakhstan or the United States, where the traces are thinnest;
- 90 days when the last 90 days and the observations older than 180 days disagree by at least 10 km/h, with at least 24 observations in all and 10 recent ones in that window;
- 180 days otherwise.
It does not invent speeds. It chooses between three predictions that are each easy to inspect, and it can be rolled back by exporting HL180 again. On the full benchmark it improved the global error by 0.0075 km/h against HL180, as expected for a change that touches few roads, and by 0.029 km/h on the roads where recent and old observations disagree, with no regression globally, on service roads, or on the sparse roads of Kazakhstan, Russia and the United States.
In the June export it chose:
HL365 HL180 HL90
day 27,186,418 15,045,541 61,779
night 31,108,322 11,180,226 5,190
About 69% of the choices are the long memory, 31% the default, and fewer than 0.1% the short one.
Weekly shape against fresh speeds
In May we had tried a weekly speed profile for 86,466 well-observed roads, and it beat the day and night speeds of that time. With freshness in place we ran the comparison again, on the same kind of held-out periods. The weekly profiles won on 47.7% of the roads in the latest 28-day period and on 48.5% over all four, with a mean error 0.054 km/h worse than the fresh scalar speeds. They lost exactly where freshness helps, on roads whose recent behaviour differs from their history, so we did not extend them.
The June export
Valhalla's traffic files address edges by graph ID, and a graph ID is a tile plus the index of the edge inside the tile. A file built for one graph build and loaded into another attaches speeds to the wrong edges without any error. So the June export was built from observations matched to the same graph build that received it: 1,282,056,165 observations, of which 16,202 had no time zone and were left out with an explicit override, 42,293,738 directed edges with at least three observations, 52,871 tile files. Valhalla's loader accepted all of them:
Parsed 42293738 constrained traffic speeds.
Parsed 42293738 free flow traffic speeds.
Updated 42293738 directed edges.
Duplicate count 0.
Speeds below lower bound count 0.
Speeds above upper bound count 0.
Parsed equals updated, which is the check that matters for a file like this.
To try the routing stack, create an API key. To discuss a Valhalla deployment, write to [email protected].
