Skip to main content
AI11 min read

Forecasting Is Now a Library Call: Notes from Running TimesFM 2.5

Google's TimesFM 2.5 shrank to 200M parameters, topped GIFT-Eval zero-shot at release, and now sits behind BigQuery's AI.FORECAST — so I ran it myself on telemetry-shaped series. Zero-shot, it beat seasonal-naive by 15% in 0.6s per forecast on CPU. Then I added a 60% level shift one week before the forecast and the one-line baseline won by 1.5x — while the model's quantile band quietly widened 2.6x. These are my notes on what caught the attention, what to observe before trusting it, and the use cases where I would actually wire it in.

All Posts
2/4

I gave a 200-million-parameter model five weeks of fake telemetry it had never seen — hourly request counts with a daily cycle, a weekend dip, and a mild upward trend — and asked for the next week. No training, no tuning, no hint about the data's frequency. It beat a seasonal-naive baseline by 15% and produced each forecast in 0.6 seconds on the CPU of the machine I ran it on. Then I moved the series to a new level one week before the forecast, the way a launch or a migration moves real traffic, and the ranking flipped: the dumbest baseline I know won by a factor of 1.5.

Both results are the point of these notes. TimesFM 2.5 is the strongest argument yet that forecasting is becoming a library call rather than a modeling project — and the failure shape it showed me is exactly the one to check for before wiring it into anything that matters.

What got everyone's attention

TimesFM started as a research bet that the LLM recipe transfers to time series. The original Google Research post (February 2024, with the paper at ICML 2024) describes a decoder-only transformer that reads a series as patches — 32 time-points in, 128 out per step — pretrained on around 100 billion real-world time-points, much of it Google Trends and Wikipedia pageviews. The claim that made people look up: zero-shot, it approaches models like DeepAR and PatchTST that were trained on the target dataset. Train once on everything, forecast anything — the same inversion language models pulled on NLP, where per-task training collapsed into pretrain-then-prompt.

Version 2.5, released September 15, 2025, is why I finally ran it. Three changes stand out from the release notes. The model shrank from 2.0's 500M parameters to 200M while getting more accurate. The maximum context grew from 2,048 points to 16,384 — enough to hand it two years of hourly data. And the awkward parts of the old API fell away: no more frequency indicator to guess at, plus an optional 30M-parameter quantile head that emits continuous quantiles out to a 1,000-step horizon. On GIFT-Eval, the community benchmark for general forecasting, Google reported 2.5 leading the zero-shot foundation models on both MASE and CRPS at release — point accuracy and probabilistic accuracy at once. That lead did not hold. Chronos-2, published a month later in October 2025, reports state-of-the-art GIFT-Eval results ahead of TimesFM 2.5 on both metrics, and takes covariates natively through in-context learning rather than a side channel. Read the top of that leaderboard as a moving target, not a property of any one model. The license is Apache-2.0.

The stronger tell that this stopped being a research artifact: in November 2025, Google wired TimesFM 2.5 into BigQuery and AlloyDB as SQL functions — AI.FORECAST is GA in BigQuery, anomaly detection is in preview, and the model behind them is now trained on over 400 billion time-points. Forecasting a table became one function call sitting where the data already lives. When a capability crosses from "clone the repo" to "it is a SQL function," backend engineers inherit it whether they asked or not.

Five weeks of fake telemetry and a baseline with amnesia

Papers benchmark on retail, traffic, and weather. I care about series shaped like the ones backend work actually produces, so my test harness generates hourly telemetry — base load around 1,000 requests per hour, a sinusoidal daily cycle, a 25% weekend dip, a slow upward trend, and noise. Two scenarios: a steady one, and one where the level jumps 60% one week before the forecast starts and stays there, the way traffic moves after a launch.

The baseline is seasonal-naive: repeat last week, verbatim. It is the honest opponent for any forecasting system, because it costs one line and no model that loses to it deserves the word "model" in production.

python
"""TimesFM 2.5 zero-shot experiment: telemetry-shaped series vs seasonal-naive."""
import time

import numpy as np

RNG = np.random.default_rng(42)
HOURS_PER_WEEK = 168
CONTEXT = 5 * HOURS_PER_WEEK   # 5 weeks of hourly history
HORIZON = HOURS_PER_WEEK       # forecast 1 week ahead


def telemetry(n, base=1000.0, trend_per_hour=0.05, break_at=None, break_factor=1.0):
    """Hourly series: daily + weekly seasonality, mild trend, noise, optional level shift."""
    t = np.arange(n)
    daily = 0.35 * np.sin(2 * np.pi * (t % 24) / 24 - 1.2)
    weekly = np.where((t // 24) % 7 >= 5, -0.25, 0.0)   # weekend dip
    level = base + trend_per_hour * t
    series = level * (1.0 + daily + weekly)
    if break_at is not None:
        series[break_at:] *= break_factor
    return series + RNG.normal(0, 0.03 * base, n)


def seasonal_naive(context, horizon, season=HOURS_PER_WEEK):
    reps = int(np.ceil(horizon / season))
    return np.tile(context[-season:], reps)[:horizon]


def mae(a, b):
    return float(np.mean(np.abs(a - b)))


def main():
    import timesfm

    model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
        "google/timesfm-2.5-200m-pytorch")
    model.compile(
        timesfm.ForecastConfig(
            max_context=CONTEXT, max_horizon=HORIZON,
            normalize_inputs=True, use_continuous_quantile_head=True,
            fix_quantile_crossing=True,
        )
    )
    scenarios = {
        "steady": telemetry(CONTEXT + HORIZON),
        "regime-break": telemetry(CONTEXT + HORIZON, break_at=CONTEXT - HOURS_PER_WEEK,
                                  break_factor=1.6),
    }
    for name, series in scenarios.items():
        context, actual = series[:CONTEXT], series[CONTEXT:]
        start = time.monotonic()
        point, quantiles = model.forecast(horizon=HORIZON, inputs=[context])
        elapsed = time.monotonic() - start
        tfm_mae = mae(point[0], actual)
        naive_mae = mae(seasonal_naive(context, HORIZON), actual)
        q = quantiles[0]  # (horizon, 10): mean + deciles q10..q90
        spread = float(np.mean(q[:, 9] - q[:, 1]))  # avg q90-q10 band width
        print(f"[{name}] TimesFM MAE={tfm_mae:.1f}  seasonal-naive MAE={naive_mae:.1f}  "
              f"ratio={tfm_mae / naive_mae:.2f}  q10-q90 width={spread:.1f}  "
              f"forecast time={elapsed:.1f}s")


if __name__ == "__main__":
    main()

Run it with python timesfm_experiment.py after pip install "timesfm[torch]" — the first run downloads the checkpoint from Hugging Face. Two lines deserve a comment. The ForecastConfig is where 2.5's ergonomics show: normalize_inputs handles scaling, the continuous quantile head is a flag, and there is no frequency parameter left to lie to. And the quantile output has shape (horizon, 10) — the mean plus deciles q10 through q90 — which turns out to be the most valuable thing the model emits.

On my machine (Python 3.11, torch 2.11, CPU only) the output was:

[steady] TimesFM MAE=27.8 seasonal-naive MAE=32.7 ratio=0.85 q10-q90 width=102.0 forecast time=0.6s [regime-break] TimesFM MAE=56.2 seasonal-naive MAE=37.6 ratio=1.49 q10-q90 width=268.2 forecast time=0.6s

The steady scenario is the headline the papers promised, reproduced on my desk: 15% better than seasonal-naive, zero-shot, on a series the model never saw, with a q10–q90 band about ±5% of the level, in 0.6 seconds on CPU. No training pipeline, no hyperparameters, no GPU.

The regime break is where it gets instructive, and the daily breakdown shows a sharper picture than the aggregate. The model does not miss the new level — its worst day is the first one, where it forecasts a daily mean of 1,594 against an actual of 1,675, still hedging toward the five weeks of lower history it was handed. It then adapts, tracks the new weekday level to within roughly 1–2%, and runs 2–6% hot across the weekend dip. Seasonal-naive, which remembers exactly one week — all of it post-break — sits closer to reality on almost every day. Amnesia, in this one situation, is a feature: the baseline cannot be distracted by the old regime because it never saw it.

The model's redeeming behavior is that it flags its own confusion. The q10–q90 band that averaged 102 requests per hour in the steady scenario averaged 268 after the break, and grew from 225 on day one to 377 by day seven. The point forecast lost to a one-liner; the uncertainty estimate told me not to trust the point forecast. The figure below shows both panels — the hourly forecast riding the new regime, and the daily means exposing the hedge.

What to observe before trusting it

My run surfaced the first caution directly: the model extrapolates patterns, not causes. A level shift it has only seen for one week gets averaged against everything before it. In production terms, the week after every launch, migration, or pricing change is exactly when the forecast is least reliable — and that is also when everyone most wants a forecast.

The second follows from it: treat the quantile band as the primary output, not the decoration. A point forecast wired into an autoscaler is a decision with no error bars; the same forecast gated on band width is a decision that knows when to abstain. In my regime-break run the band widened 2.6x before the point error materialized as a problem — that signal is free, and the fix_quantile_crossing flag means the deciles arrive already ordered.

Third, the model is univariate per series. It reads one stream of numbers; it cannot know a deploy shipped, a marketing campaign started, or a holiday is coming, unless that information is already visible as history in the series itself. Covariate support exists as an XReg add-on restored in October 2025, but the zero-shot magic path is history-in, forecast-out. Any series whose future is driven by events outside its own past will punish that blindness.

Fourth, hold the benchmark numbers loosely. A paper from October 2025 on evaluation of time-series foundation models argues that as pretraining corpora swallow most public time-series data, train-test overlap and temporal correlation make zero-shot leaderboards prone to "overly optimistic performance estimates that fail to generalize to real-world settings." I do not read that as an accusation against any one model — it is a structural problem of the category, the same contamination debate LLM benchmarks went through. The practical consequence is the pattern I used above: your own holdout, against seasonal-naive, on your own series, is the only benchmark that settles anything. The harness is under 80 lines; there is no excuse to skip it.

Where I would actually use it

The use cases I would reach for first share one shape: seasonal, slowly-trending series where a week-ahead or day-ahead level estimate changes a decision, and where being wrong is recoverable.

Capacity planning and pre-scaling is the obvious one. Forecast tomorrow's hourly peak from six weeks of history and warm capacity ahead of the curve instead of reacting to it; a reactive autoscaler stays as the safety net, so a bad forecast costs money rather than availability. Queue and backlog planning is the same math I worked through in my earlier notes on backlog-recovery arithmetic: drain time depends on the arrival curve you expect, and a forecast of arrivals turns "will this backlog clear before the evening peak" from a guess into an inequality. Cache prewarming before a predicted traffic ramp buys the same latency win as prewarming by cron, minus the cron's ignorance of trend. And error-budget projection — forecasting a burn-rate series to see whether the budget survives the month — is a decision that tolerates fuzziness and benefits from the quantile band directly.

If the data already lives in BigQuery, the calculus tilts further: AI.FORECAST makes the experiment a single query with no infrastructure at all, which is the cheapest possible way to find out whether a foundation model earns its keep on your series. The same GA release shipped AI.EVALUATE, which scores a forecast against held-out actuals — the holdout comparison I argued for above, as a second query. Pass model => 'TimesFM 2.5' explicitly; the default is not 2.5.

Where I would not use it: anything with a sub-second reaction contract (that is anomaly detection's job, not forecasting's); series dominated by external events the model cannot see — promotions, deploys, incident traffic; the first week after any known regime change, per my own results above; and any series where the seasonal-naive ratio comes back near 1.0, because a model that ties a one-liner is pure operational overhead.

Takeaways

  • Zero-shot on steady telemetry, TimesFM 2.5 beat seasonal-naive by 15% in 0.6 seconds on CPU — no training pipeline, no GPU, no frequency hint to supply.
  • One week after a 60% level shift, the same model lost to seasonal-naive by 1.5x. The week after a launch or migration is when a forecast is least trustworthy and most wanted.
  • Treat the q10–q90 band as the primary output. It widened 2.6x on the broken regime before the point error mattered — a free abstain signal for anything reading the forecast.
  • The model is univariate: deploys, promotions, and holidays are invisible unless they already appear as history in the series.
  • Leaderboard position is temporary — Chronos-2 took the GIFT-Eval lead a month after 2.5 shipped. Run the harness on your own holdout instead; a seasonal-naive ratio near 1.0 means the model is pure operational overhead.

The decision rule I am keeping from this: run the harness against seasonal-naive on your own holdout — the ratio decides whether the model deserves a place in the loop, and the quantile band decides, hour by hour, whether to believe it. The experiment I have queued next is the covariate path: whether XReg with a deploy-marker series closes the regime-break gap, or whether that first week after a launch simply belongs to humans. Chronos-2 turns that into a two-model question, since it takes covariates in-context rather than through a separate add-on.

Read next

Still here? You might enjoy this.

Nothing close enough — try a different angle?

Was this helpful?

Leave a rating or a quick note — it helps me improve.