You're watching a Formula 1 race. Twenty-two cars flying around a circuit, each producing telemetry about speed, braking, and pace every second.
But what if, while the race is happening, you could take all of that data and answer a much more interesting question:
Who's going to win?
That's what I built.
A real-time F1 prediction pipeline, powered by ClickHouse, that watches a race unfold lap by lap and predicts who could win in real-time.

So, how does it work?
I didn't want another dashboard showing what already happened. F1 has plenty of those.
I wanted something that could take the current state of a race and then predict what happens next. At any point, I wanted to know who’s most likely to win or who’s most likely to pit soon.
And this had to be done while the race was still live. That meant ingesting telemetry, engineering features, running ML models, simulating outcomes, and updating a dashboard. Fast enough to still matter. Sounded straightforward. It wasn’t.
The data problem
There were millions of rows of data for every race. I collected telemetry from 79 Grand Prix, resulting in about 118 million rows of historical telemetry, plus a live feed of each driver's speed, braking, tires, and position as a race unfolds.
So…how do you store and analyse such a humungous historical dataset without trading off speed in real-time? For me, the answer was ClickHouse.
I needed to query this data fast - leaderboards, lap-time comparisons and tire compound analysis - all mid-race. A query building a full ranked leaderboard across every driver and lap ran in 41ms. Median latency across the analytical workload was about 23ms.
I thought, hey, this is great! But of course, fast queries alone weren't enough. I still needed to run the ML.

Putting ML inside the database
Normally, running ML on live data means shipping it somewhere else and getting the prediction results from there. That works, but every hop adds latency. I wanted to know if the hops could disappear entirely, if the model could just live inside the query.
So I did exactly that.
Feature encoding became six SQL UDFs - f1_compound_encoder(), for instance, turns SOFT à 0, MEDIUM à 1, HARD à 2, with the same pattern for teams, track types, tire wear, stint length, and speed thresholds.

The ML inference followed the same idea, but through executable UDFs. Instead of sending the engineered features to a separate model-serving service, I exposed each model as an executable function inside ClickHouse: one for win probability, one for pit likelihood, and one for pace.
Each executable UDF is backed by a small Python script sitting behind a SQL function name. When ClickHouse calls the function, it passes the row's feature values to the script, which runs the exact same trained XGBoost or scikit-learn model artifact used elsewhere in the pipeline.
And no, it's not a lighter or approximate version of the model. It's the same model, just invoked from a different place.
Raw analytics queries were fast - no surprise there. That's what ClickHouse is built for. What's more interesting is that, in my tests, running inference through the UDF path added surprisingly little overhead.
Individual predictions still weren't the answer to the real question, though.
Predicting the rest of the race
What if Leclerc’s pit stop was too long or Norris’ tires wore out sooner than expected? You can never be sure of what might happen next in a Formula 1 race.
A race isn't a pile of independent predictions. A fast driver can lose it all in the pits. Someone staying out longer might benefit from fresher tires later. A small pace shift over a few laps rewrites the whole order.
To handle such scenarios, I built a Monte Carlo simulator. Every time new data arrives, it takes the current state of all 22 drivers and simulates the rest of the race, thousands of times. Each run assigns predicted lap times with uncertainty, applies tire degradation, and triggers pit stops based on the pit model's probabilities. Every simulation plays out differently; someone pits early, someone stays out longer, and the finishing order shifts.
Aggregate the simulated outcomes and you get probabilities. If a driver wins 335 out of 500 simulations, their predicted win probability is 67%. The same process gives podium odds, expected finishing positions, and time gaps, recalculated every time the race state changes.


From telemetry to prediction
A prediction that arrives after the race has moved on is useless. So the whole pipeline had to be fast:
Live Telemetry à ClickHouse à Feature Engineering à ML Inference à Monte Carlo Simulation à prediction_results à WebSocket à Dashboard
ClickHouse query latency: ~23 ms. Post-processing: ~45 ms. Monte Carlo simulation: 0.6-1s. From new telemetry to an updated dashboard: under two seconds. A driver braking hard, extending a stint, or pitting unexpectedly shows up in thousands of freshly simulated outcomes within seconds.
Adding an AI analyst
Once live predictions existed in one place, I realised I could build on top of them. Instead of making someone read the dashboard and interpret probabilities themselves, I connected an LLM directly to the prediction_results table using LibreChat and MCP.
It's not reading a static report, it's querying the same live data as the dashboard, updating as the race does.

Keeping it honest (drift detection and model monitoring)
A model that's never checked against reality is just a confident guess. Since prediction_results already logs every forecast and the features behind it, the two checks that matter most are just a few SQL queries away.
The first is calibration: when the model gives a driver a 70% chance of winning, do drivers with similar predictions actually win roughly 70% of the time?
By bucketing final-lap win_proba values and joining them against actual race results, I can compare predicted probability with observed win rate:
SELECT
round(win_proba, 1) AS proba_bucket,
count() AS predictions,
avg(actual_win) AS observed_win_rate
FROM (
SELECT
p.event, p.year, p.driver_code, p.win_proba,
if(coalesce(r.final_position, 0) = 1, 1, 0) AS actual_win
FROM prediction_results p
INNER JOIN f1_race_results r
ON p.event = r.event AND p.year = r.year AND p.driver_code = r.driver_code
WHERE p.lap_no = (
SELECT max(lap_no) FROM prediction_results
WHERE event = p.event AND year = p.year AND driver_code = p.driver_code
)
)
GROUP BY proba_bucket
ORDER BY proba_bucket;
The second check is drift. Race over race, I track changes in features such as pace, degradation, and pit probability:
SELECT
event, year,
avg(delta_vs_field) AS avg_pace_delta,
stddevPop(delta_vs_field) AS pace_spread,
avg(obs_deg) AS avg_degradation,
quantile(0.95)(pit_prob) AS p95_pit_prob
FROM prediction_results
GROUP BY event, year
ORDER BY year, event;
A regulation change, new tire compound, or unfamiliar track can shift these distributions before accuracy visibly drops.
I ran both checks against the three finished 2026 races currently in the system: 44 driver-race predictions scored against real outcomes. It's far too small a sample to draw conclusions, but that's not the point yet. The monitoring harness works: the same tables powering the live dashboard can also tell me, over a full season, whether the model's confidence actually means anything.
So, what did I end up building?
- 118 million rows of historical F1 telemetry from 79 Grand Prix
- ClickHouse queries with a median latency of 23 ms
- Six SQL UDFs for feature engineering
- Two inference paths - a live Python pipeline and the executable UDFs
- A Monte Carlo engine simulating thousands of possible races
- A live dashboard updating in under two seconds
- An AI analyst reading from the same live data through MCP and LibreChat

One idea holds the entire thing together:
Keep the data, analytics, and predictions as close together as possible.
Instead of shuttling data between a database, a feature store, a model server, and a prediction service, I made ClickHouse the centre of the entire pipeline.
It stores the telemetry. It powers the analytics. It prepares the features. And, through the UDF path, it can score the models directly from SQL.
From there, thousands of possible races are simulated, predictions are written back to the database, the dashboard updates, and an AI analyst can query the exact same live results.
What started as an idea to predict an F1 race ended up becoming an experiment in how much of a real-time ML system could live in one place.
And now, while 22 cars are racing around a circuit, the system is watching them, and continuously trying to answer one question:
Who's going to win?
Source code: https://github.com/tanushree-quest1/f1prediction
